onClick handler not registering with ReactDOMServer.renderToString - javascript

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

Related

How do I set a Svelte Component's event listener and inner HTML for slot dynamically via JavaScript?

JavaScript:
const dispatchEvent = createEventDispatcher();
let dialog: Dialog;
HTML
<Dialog bind:this={dialog} on:close={() => {dispatchEvent("deletedPost", postIndex)}} modal={true} open={false}>
... Here I want to insert some HTML to be used in the slot of the "Dialog" component.
</Dialog>
My question is, how do I set the on:close and the inner HTML dynamically via JS?
I have this demand because this Dialog component should be reused for multiple purposes by its parent component. Sometimes it displays info about a post has been created, sometimes deleted, sometimes edited and so on.
And each of the cases requires different content in the component as well as a different handler for the close event emitted by the Dialog.
Should I use something like
dialog.$on = handlerFunction;
// I didn't find a possible property for setting HTML though
Thanks!
There currently is no way to programmatically interact with the slots (unless you want to investigate the Svelte internals and rely on implementation details which may change).
Either pass in a full component via the props (which can be rendered using <svelte:component>), insert only text or use {#html} to render HTML content (which is not recommended for security reasons).
(The syntax for the events is dialog.$on('close', event => ...) as detailed in the client-side API section.)
Example for all mentioned property types:
<!-- Dialog.svelte -->
<script>
import { createEventDispatcher } from 'svelte';
export let open = true;
export let component = null;
export let text = null;
export let html = null;
const dispatch = createEventDispatcher();
$: dispatch(open ? 'open' : 'close');
</script>
<div>
<h2>Dialog</h2>
{#if component}
<svelte:component this={component} />
{/if}
{#if text}
{text}
{/if}
{#if html}
{#html html}
{/if}
<div><button on:click={() => open = false}>Close</button></div>
</div>
Usage:
// ...
import Content from './Content.svelte';
const d = new Dialog({
// ...
props: { text: 'Hello world' },
});
// or
const d = new Dialog({
// ...
props: { html: '<input placeholder="Text here"/>' },
});
// or
const d = new Dialog({
// ...
props: { component: Content },
});
d.$on('close', () => d.$destroy());
REPL
As noted, #html is dangerous and ideally should only be used with XSS sanitization in place.

How update the vue template at the run-time?

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);
});

React and using ReactDOM.createPortal() - Target container is not a DOM element Error

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

How to select the clickable parent in my React component even if I click on its children or anywhere else inside the parent

I have started an application which I want to work same as weather.com next 36 hours section. The idea is when you click on each weatherCard which has a seperate component in my app you will update the below section which is my weatherDetails component based on the selected weatherCard /weather box. So I made the entire component clickable by giving it the click event via props from my stateful component which is my weatherLocation component. This is my WeatherCard component:
const WeatherCard = (props) => (
<div id={props.date} className="weatherCard" onClick={props.clicked}>
<h2 className="cardDate">{props.date}</h2>
<h4>{props.forcast}</h4>
<div className="minmaxDeg">
<data>{props.min}</data>
<data>{props.max}</data>
</div>
<data>{props.rain}</data>
</div>
);
And here in render method in WeatherLocation component I loop through data coming from state and give props the WeatherCard component:
const WeatherCards = this.state.reports.map( report => {
return(
<WeatherCard
key={report.id}
{...report}
clicked={() => this.handleCardClick(event)}
/>
);
});
And this is the handleCardClick that I added for it just for testing:
handleCardClick = event => {
// const { reports , selectedCardInfo , activeCard } = this.state;
const selectedDate = document.getElementById(event.target.id);
console.log(event.target.id);
}
I don't want to use anchor tag as I don't need href. The click works fine by itself. But because I need to get the id of the parent which is the div with the class of weatherCard. At the moment when I click on other elements inside the card I cannot get the id because they are not the parent. The reason I need its id is when I get data with from the API I need a unique value for each card so that when you click on the card the data for that card will be shown in the other component which is the WeatherDetails component. But for now I need to be able to somehow choose that selected card and pull out the state for that unique card. Could someone help me out? Thanks.
You just need to pass the Parent component ID to your onClick function in Weather Card.
Here is your WeatherCard - Component
const WeatherCard = (props) => (
<div id={props.date} className="weatherCard" onClick={event => props.clicked(event, props.id)}>
<h2 className="cardDate">{props.date}</h2>
<h4>{props.forcast}</h4>
<div className="minmaxDeg">
<data>{props.min}</data>
<data>{props.max}</data>
</div>
<data>{props.rain}</data>
</div>
);
You can see that I have added props.id to your onClick function and with help of event now you can access that id from the parent component.
Now here is your Parent Component- WeatherCards
const WeatherCards = this.state.reports.map( (report, i) => {
return(
<WeatherCard
key={report.id}
id={i}
{...report}
clicked={this.handleCardClick}
/>
);
});
You can see in the code I am passing index number as id to your child component.
So this will give you an id (for now it's an index number) of the card in your onClick handler.
and Finally, here is your on click handler.
handleCardClick = (event, weatherCardID) => {
console.log(weatherCardID)
}
As of now, I am using the index as id if you want to use a unique identifier, you can change that easily.
General JavaScript solution is to differentiate the elements and .stopPropogation after you've captured the event you are targeting. A nested unordered list, <ul>would be an example. Tag the containing <li> with an .opened class upon rendering/displaying each level of nesting, tag those <li> elements accordingly, e.g. a dataset attribute such as data-make, then data-model, then data-option. You then attach and fire event listeners on the different level <li>'s.
Thank you #RutulPatel. I made your answer as the answer. But I changed your code a bit as I got your point so I wrote an answer as it is long. I think we might not need to change the WeatherCard at all and I don't pass event or any logic there. so it will be intact:
const WeatherCard = (props) => (
<div id={props.date} className="weatherCard" onClick={event => props.clicked(event, props.id)}>
<h2 className="cardDate">{props.date}</h2>
<h4>{props.forcast}</h4>
<div className="minmaxDeg">
<data>{props.min}</data>
<data>{props.max}</data>
</div>
<data>{props.rain}</data>
</div>
);
But I use your tip changing my weatherCards array to look like this:
const weatherCards = this.state.reports.map( report => {
return(
<WeatherCard
key={report.id}
id={report.date}
{...report}
clicked={() => this.handleCardClick(event, report.date)}
/>
);
});
So I use the report.date which is a unique value as my id. Also I don't pass event as a parameter to the arrow function I just pass it with the report.date to the handler:
clicked={() => this.handleCardClick(event, report.date)}
And the handler will be the same as you did:
handleCardClick = (event, weatherCardID) => {
console.log(weatherCardID)
}
I might even remove event later on from both if there was no need fo that.
Thank you again.

React Testing: Event handlers in React Shallow Rendering unit tests

Background
I am trying to learn how to use the React Shallow Rendering TestUtil and had the tests passing until I added an onClick event handler to both; It seems that there must be some difference with the Accordion.toggle function I am trying to use in Accordion.test.js vs this.toggle in Accordian.js...but I can't figure it out.
Question
How can I get the two highlighted tests in Accordian.test.js to pass?
Steps to reproduce
Clone https://github.com/trevordmiller/shallow-rendering-testing-playground
npm install
npm run dev - see that component is working when you click "Lorem Ipsum"
npm run test:watch - see that tests are failing
There are a number of issues preventing your tests from passing.
Looking at the test "should be inactive by default":
Accordion.toggle in your test is a property of the Accordion class, and this.toggle in your code is a property of a instance of the Accordion class - so in this case you are comparing two different things. To access the 'instance' method in your test you could replace Accordion.toggle with Accordion.prototype.toggle. Which would work if it were not for this.toggle = this.toggle.bind(this); in your constructor. Which leads us to the second point.
When you call .bind() on a function it creates a new function at runtime - so you can't compare it to the original Accordion.prototype.toggle. The only way to work around this is to pull the "bound" function out of the result from render:
let toggle = result.props.children[0].props.onClick;
assert.deepEqual(result.props.children, [
<a onClick={toggle}>This is a summary</a>,
<p style={{display: 'none'}}>This is some details</p>
]);
As for your second failing test "should become active when clicked":
You try calling result.props.onClick() which does not exist. You meant to call result.props.children[0].props.onClick();
There is a bug in React that requires a global "document" variable to be declared when calling setState with shallow rendering - how to work around this in every circumstance is beyond the scope of this question, but a quick work around to get your tests passing is to add global.document = {}; right before you call the onClick method. In other words where your original test had:
result.props.onClick();
Should now say:
global.document = {};
result.props.children[0].props.onClick();
See the section "Fixing Broken setState()" on this page and this react issue.
Marcin Grzywaczewski wrote a great article with a workaround for testing a click handler that works with shallow rendering.
Given a nested element with an onClick prop and a handler with context bound to the component:
render() {
return (
<div>
<a className="link" href="#" onClick={this.handleClick}>
{this.state.linkText}
</a>
<div>extra child to make props.children an array</div>
</div>
);
}
handleClick(e) {
e.preventDefault();
this.setState({ linkText: 'clicked' });
}
You can manually invoke the function value of the onClick prop, stubbing in the event object:
it('updates link text on click', () => {
let tree, link, linkText;
const renderer = TestUtils.createRenderer();
renderer.render(<MyComponent />);
tree = renderer.getRenderOutput();
link = tree.props.children[0];
linkText = link.props.children;
// initial state set in constructor
expect(linkText).to.equal('Click Me');
// manually invoke onClick handler via props
link.props.onClick({ preventDefault: () => {} });
tree = renderer.getRenderOutput();
link = tree.props.children[0];
linkText = link.props.children;
expect(linkText).to.equal('Clicked');
});
For testing user events like onClick you would have to use TestUtils.Simulate.click. Sadly:
Right now it is not possible to use ReactTestUtils.Simulate with Shallow rendering and i think the issue to follow should be: https://github.com/facebook/react/issues/1445
I have successfully tested my click in my stateless component. Here is how:
My component:
import './ButtonIcon.scss';
import React from 'react';
import classnames from 'classnames';
const ButtonIcon = props => {
const {icon, onClick, color, text, showText} = props,
buttonIconContainerClass = classnames('button-icon-container', {
active: showText
});
return (
<div
className={buttonIconContainerClass}
onClick={onClick}
style={{borderColor: color}}>
<div className={`icon-container ${icon}`}></div>
<div
className="text-container"
style={{display: showText ? '' : 'none'}}>{text}</div>
</div>
);
}
ButtonIcon.propTypes = {
icon: React.PropTypes.string.isRequired,
onClick: React.PropTypes.func.isRequired,
color: React.PropTypes.string,
text: React.PropTypes.string,
showText: React.PropTypes.bool
}
export default ButtonIcon;
My test:
it('should call onClick prop when clicked', () => {
const iconMock = 'test',
clickSpy = jasmine.createSpy(),
wrapper = ReactTestUtils.renderIntoDocument(<div><ButtonIcon icon={iconMock} onClick={clickSpy} /></div>);
const component = findDOMNode(wrapper).children[0];
ReactTestUtils.Simulate.click(component);
expect(clickSpy).toHaveBeenCalled();
expect(component).toBeDefined();
});
The important thing is to wrap the component:
<div><ButtonIcon icon={iconMock} onClick={clickSpy} /></div>
Hope it help!

Categories