I am working on the CMS based vue page. In the page I have one root container inside that I have two child container as looks below
<div id="app">
<div class="above-the-fold">...</div>
<div class="below-the-fold noscript-container">
<noscript>
<div v-if="true">{{someDynamicBinding}}</div>
<div v-if="false">{{someDynamicBinding}}</div>
</noscript>
</div>
</div>
If javascript is enabled I am removing noscript tag and appending inside contents to the parent element using a below script.
document.addEventListener("DOMContentLoaded", function () {
const appendContents = function () {
const noScript = document.querySelector(".noscriptcontainer noscript");
const belowContent = noScript.innerHTML;
noScript.parentElement.innerHTML += belowContent;
console.log("elm appended");
window.removeEventListener("scroll", appendContents);
noScript.remove();
console.log("eve removed");
};
window.addEventListener("scroll", appendContents);
});
Now the problem is vue doesn't evaluating v-if or {{dynamicBinding}}
Jsfiddle Link https://jsfiddle.net/69yr3mp5/2/
Now what is the best way to make this work?
Try to replace the second if with v-else-if
<div v-if="true"><h1>
{{someDynamicBinding}}
</h1></div>
<div v-else-if="false"><h1>
{{someDynamicBinding}}
</h1></div>
</noscript>
TL;DR;
You can update the template run-time, but you can't update the template after component is mounted.
Run-time, in the context of these frameworks, means that it is not pre-compiled, which is something that Vue supports. However, the problem here (one of them anyway) is that you are modifying the template after the application is mounted. The Vue app will not continue to check the template after the initial read. While I'm not sure what you're trying to accomplish here, It seems that the approach is wrong.
To illustrate, if you use this code, it will compile the updated code, but not until you scroll.
let mounted = false
document.addEventListener("DOMContentLoaded", function() {
const appendContents = function() {
const noScript = document.querySelector(".noscript-container noscript");
const belowContent = noScript.innerHTML;
noScript.parentElement.innerHTML += belowContent;
console.log("elm appended");
window.removeEventListener("scroll", appendContents);
noScript.remove();
console.log("eve removed");
if (!mounted) {
Vue.createApp({
data() {
return {
someDynamicBinding: 'Some Content'
};
},
mounted() {
console.log('mounted')
},
}).mount('#app');
};
mounted = true
}
window.addEventListener("scroll", appendContents);
});
Related
Is it possible to execute a <Script/> every time the props of a react/nextjs component change?
I am converting markdown files to html using marked and, before rendering the html, I would like to have a [copy] button on each <pre> block (those are the code blocks). I have a <script/> that iterates through the <pre> blocks of the DOM document.querySelectorAll("pre") and injects the button needed. If the html changes though at a later stage, then I have found no way to re-run the script to add the copy buttons again.
I have the impression that this is not a very react/nextjs way of doing this, so any hints would be appreciated.
The Script to add the copy buttons. I have added this as the last tag of my <body>:
<Script id="copy-button">
{`
let blocks = document.querySelectorAll("pre");
blocks.forEach((block) => {
if (navigator.clipboard) {
let button = document.createElement("img");
button.src = "/images/ic_copy.svg"
button.title = "Copy"
button.id = "copy"
button.addEventListener("click", copyCode);
block.appendChild(button);
}
});
async function copyCode(event) {
const button = event.srcElement;
const pre = button.parentElement;
let code = pre.querySelector("code");
let text = code.innerText;
await navigator.clipboard.writeText(text);
button.src = "/images/ic_done.svg"
setTimeout(()=> {
button.src = "/images/ic_copy.svg"
},1000)
}
`}
</Script>
the React component. Not much to say here. The content is coming from the backend. Not sure what would be the 'React' way to do this without the script.
export default function Contents({ content }) {
return (
<div className='pl-2 pr-2 m-auto w-full lg:w-2/3 mb-40 overflow-auto break-words'>
<div className="contents" dangerouslySetInnerHTML={{ __html: content }} />
</div>
)
}
You should absolutely not do this and instead incorporate this logic into your react app, but if you must you can leverage custom window events to make logic from your html script tags happen from react.
Here is an example script:
<script>
function addEvent() {
function runLogic() {
console.log("Stuff done from react");
}
window.addEventListener("runscript", runLogic);
}
addEvent();
</script>
And calling it form react like this:
export default function App() {
const handleClick = () => {
window.dispatchEvent(new Event("runscript"));
};
return (
<div className="App" onClick={handleClick}>
<h1>Hello</h1>
</div>
);
}
I have a form fragment wrapped in a component that is hidden by v-if. When the user clicks a button, it toggles the boolean, revealing the hidden component, and when that happens I'd like to transfer focus to the first form input on the fragment.
I've tried using aria-live to no avail. I suspect the nature of the SPA interferes with the registration of those live regions (meaning my guess is that they must be registered when the page is rendered, as they don't seem responsive when injected into the DOM). I did not however, chase the answer down a rabbit hole so that is speculative. So then I added a class to the target input and tried to use HTMLElement.focus()
document.querySelector('.focus')[0].focus();
This also did not work. Does anyone know of a reason why I cannot seem to focus on an element that was recently injected into the DOM and is visible on the page at the time?
I think what's needed is for your form component to have something defined for when it's mounted:
Vue.config.productionTip = false;
const template = `<div>
<div>
<inner v-if="showInner" />
<button #click="toggle">Toggle inner component</button>
</div>
</div>`
const inner = {
name: 'inner',
template: '<form><input ref="textInput" type="text"/></form>',
mounted() {
this.$refs.textInput.focus()
}
};
new Vue({
template,
data: function() {
return {
showInner: false
};
},
methods: {
toggle() {
this.showInner = !this.showInner;
}
},
components: {
inner
}
}).$mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app"></div>
I'm working on a React app and am trying to use ReactDOM.createPortal() to add html content to a div that is outside the component (called ToDoItem).
{ReactDOM.createPortal(
<Route path={`/profile/project/${this.props.project._id}`} render={() =>
<ProjectView project={this.props.project}/>} />,
document.getElementById('tasks')
)}
None of the HTML in the public folder is predefined - it is all dynamically created.
I think this could be the cause of the error: React tries to add HTML to the div with the id of tasks which, but the div is not loaded into the DOM before this happens?
If this method is incorrect, is there any other method I can use append html content to another div outside the component?
Some other info: the component from which I tried to run this method is a stateless component, not a class component.
This is the error:
You can wait until the DOM is ready using React.useEffect, and then you call ReactDOM.createPortal:
function Component() {
const [domReady, setDomReady] = React.useState(false)
React.useEffect(() => {
setDomReady(true)
})
return domReady
? ReactDOM.createPortal(<div>Your Component</div>, document.getElementById('container-id'))
: null
}
The problem is that you can't createProtal to react component.
the second parameter have to be dom elemenet, not react created element
I had this issue because I forgot to add <div id="some-id"></div> to my index.html file :-D
So just as a reminder for anyone who has a similar problem or doesn't know how to use React portals ( to create a modal for example):
in modal.jsx:
const modalRoot = document.getElementById('modal');
const Modal = () => {
const modalElement = document.createElement('div');
// appends the modal to portal once modal's children are mounted and
// removes it once we don't need it in the DOM anymore:
useEffect(() => {
modalRoot.appendChild(modalElement);
return () => {
modalRoot.removeChild(modalElement);
};
}, [modalElement]);
return createPortal(<div>modal content</div>, modalRoot);
};
in index.html:
<!DOCTYPE html>
<html lang="en">
<head>
// head content
</head>
<body>
<div id="root"></div>
// dont't forget about this:
<div id="modal"></div>
</body>
</html>
Before first render of components, there is no element in the DOM and document.getElementById cannot reach to any element. elements added to DOM after first render.
Use useRef in parent component and send it to Portal Component.
In parent component:
const tasksRef = React.useRef(null);
const [tasksSt, setTasksSt]= React.useState();
....
<div ref={
(current) => {tasksRef.current = current;
setTasksSt(tasksRef.current);
}
}/>
<YourPortalComp tasksRef={tasksRef} />
In Portal Component
{this.props.tasksRef.current?(ReactDom.createPortal(<...>, this.props.tasksRef.current):null}
If the target element (In your case element with id tasks) is loaded but still you are getting the Target container is not a DOM element Error error, you can try the below solution.
const MyElement = () => {
const [containerToLoad, setContainerToLoad] = useState(null);
// If the target element is ready and loaded
// set the target element
useEffect(() => {
setContainerToLoad(document.getElementById('container'));
}, []);
return containerToLoad && createPortal(<div>modal content</div>, containerToLoad);
};
check your id in index.html file,
So lets say, your index.html file has:
<div id="overlays"></div>
Your Modal should point to same div, ie
const portalElement = document.getElementById('overlays');
const Modal = (props) => {
return (
<Fragment>
{ReactDOM.createPortal(<Backdrop onClose={props.onClose} />, portalElement)}
</Fragment>
);
};
For those that might be using Next.js, an equivalent to AlexxBoro's answer for next js can be found here. https://www.learnbestcoding.com/post/101/how-to-create-a-portal-in-next-js
I have following kind of code:
<div>
<compA />
<compB />
</div>
How do I make sure that first compA is rendered only after it compB is rendered.
Why I want is I have some dependency on few elements of compA, and style of compB depends on presence of those elements.
Why in details:
I have some complex UI design, where one box will become fixed when you scroll. SO It will not go above the screen when you scroll, it will be fixed once you start scrolling and it start touching the header. So I am using jquery-visible to find if a div with a particular id is visible on the screen, if it is not visible, I change the style and make that box fixed. Following code should give the idea what I am doing:
methods: {
onScroll () {
if ($('#divId').visible(false, false, 'vertical')) { // This is div from the compA, so I want to make sure it is rendered first and it is visible
this.isFixed = false
} else {
this.isFixed = true
}
}
},
mounted () {
window.addEventListener('scroll', this.onScroll() }
},
destroyed () {
window.removeEventListener('scroll', this.onScroll)
}
I dont want to make those in same component as one reason is it dont make sense as the nature of these components, and other I use compA at many places, while compB is specific to only one page. Also layout of these does not allow me to make compB child of compA as suggested in comments.
Any suggestions are welcome.
An option with events:
<!-- Parent -->
<div>
<comp-a #rendered="rendered = true"></comp-a>
<component :is="compB"></component>
</div>
<script>
// import ...
export default {
components: { CompA, CompB },
watch: {
rendered: function (val) {
if (val) this.compB = 'comp-b';
}
},
data() {
return {
rendered: false,
compB: null
}
}
}
</script>
<!-- Component B -->
<script>
export default {
mounted() {
this.$emit('rendered');
}
}
</script>
After going through the edit I realised that the dependency is not data driven but event driven (onscroll). I have tried something and looks like it works (the setTimeout in the code is for demonstration).
My implementation is slightly different from that of Jonatas.
<div id="app">
RenderSwitch: {{ renderSwitch }} // for demonstration
<template v-if='renderSwitch'>
<comp-a></comp-a>
</template>
<comp-b #rendered='renderSwitchSet'></comp-b>
</div>
When the component-B is rendered it emits an event, which just sets a data property in the parent of both component-A and component-B.
The surrounding <template> tags are there to reduce additional markup for a v-if.
The moment renderSwitch is set to true. component-a gets created.
I am trying to copy this fiddle:
http://jsfiddle.net/jhudson8/135oo6f8/
(I also tried this example
http://codepen.io/adamaoc/pen/wBGGQv
and the same onClick handler problem exists)
and make the fiddle work for server side rendering, using ReactDOMServer.renderToString
I have this call:
res.send(ReactDOMServer.renderToString((
<html>
<head>
<link href={'/styles/style-accordion.css'} rel={'stylesheet'} type={'text/css'}></link>
</head>
<body>
<Accordion selected='2'>
<AccordionSection title='Section 1' id='1'>
Section 1 content
</AccordionSection>
<AccordionSection title='Section 2' id='2'>
Section 2 content
</AccordionSection>
<AccordionSection title='Section 3' id='3'>
Section 3 content
</AccordionSection>
</Accordion>
</body>
</html>
)));
the Accordion element looks like so:
const React = require('react');
const fs = require('fs');
const path = require('path');
const Accordion = React.createClass({
getInitialState: function () {
// we should also listen for property changes and reset the state
// but we aren't for this demo
return {
// initialize state with the selected section if provided
selected: this.props.selected
};
},
render: function () {
// enhance the section contents so we can track clicks and show sections
const children = React.Children.map(this.props.children, this.enhanceSection);
return (
<div className='accordion'>
{children}
</div>
);
},
// return a cloned Section object with click tracking and 'active' awareness
enhanceSection: function (child) {
const selectedId = this.state.selected;
const id = child.props.id;
return React.cloneElement(child, {
key: id,
// private attributes/methods that the Section component works with
_selected: id === selectedId,
_onSelect: this.onSelect
});
},
// when this section is selected, inform the parent Accordion component
onSelect: function (id) {
this.setState({selected: id});
}
});
module.exports = Accordion;
and the AccordionSection component looks like so:
const React = require('react');
const AccordionSection = React.createClass({
render: function () {
const className = 'accordion-section' + (this.props._selected ? ' selected' : '');
return (
<div className={className}>
<h3 onClick={this.onSelect}>
{this.props.title}
</h3>
<div className='body'>
{this.props.children}
</div>
</div>
);
},
onSelect: function (e) {
console.log('event:',e);
// tell the parent Accordion component that this section was selected
this.props._onSelect(this.props.id);
}
});
module.exports = AccordionSection;
everything works, and the CSS is working, but the problem is that the onClick doesn't get registered. So clicking on the accordion elements does nothing. Does anyone know why the onClick handler might not get registered in this situation?
React DOM render to string only sends the initial HTML as a string without any JS.
You need a client side react router as well which will attach the required JS handlers to the HTML based on their react-id's. The JS needs to run on both sides.
Universal rendering boilerplate for quick start. https://github.com/erikras/react-redux-universal-hot-example
Another question which is similar to yours. React.js Serverside rendering and Event Handlers
None of the hooks will register with ReactDOMServer.RenderToString. If you want to accomplish server side rendering + hooks on your react component, you could bundle it on the client (webpack, gulp, whatever), and then also use ReactDOMServer.RenderToString on the server.
Here's a blog post that helped me accomplish this:
https://www.terlici.com/2015/03/18/fast-react-loading-server-rendering.html