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!
Related
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.
Note: I have edited the question after the changes I have made according to Nicholas Tower's answer.
I have a global function which bound to a component and changes it's state.
I want to build a form builder system. There is a global function named setLabel which is bound to a component named InputBox and changes it's state. This global function is triggered via another component named ModalPanel which controls the editable properties on the bound component InputBox. I have simplified the function and component class for simplicity of this question.
Here is the global function:
function setLabel(postID, attributeName ){
var text = 'example';
if(text !== ''){
this.setState({ [attributeName] : text});
}
}
And here is the component which is bound to the setLabel function. Notice how setLabel function is passed from parent InputBox component to child ModalPanel component function as a property.
class InputBox extends Component{
constructor(props){
super(props);
this.state = {
placeholder : '',
maxlength: '',
type: '',
}
this.setLabel = setLabel.bind(this); // Binding this to the global function.
}
render(){
let elementId = "inputElement-" + this.props.idCounter;
let mainElement = <Form.Control
id = {elementId}
type = {this.state.type}
placeholder = {this.state.placeholder}
maxLength = {this.state.maxlength}
/>
return <Row>
<ModalPanel
handleHide = {this.props.handleHide}
handleShow = {this.props.handleShow}
setLabel = {this.setLabel}
/>
</Row>
}
}
Lastly, below is the ModalPanel component function where the setLabel function is triggered.
function ModalPanel(props){
return(
......................
......................
......................
<Button variant="primary" onClick = {() => props.setLabel()}>Save changes</Button>
......................
......................
......................)
}
setLabel function which is aimed to set the state of InputBox must be triggered when a button is clicked in the ModalPanel component. The problem is, there are multiple rendered <InputBox /> components on the window and when I try to use this functionality, "the state change" only affect the first instance of <InputBox /> component. What I want to do is that, every instance should have their own internal state and setLabel() function should be bound to the specific component from where it is called. So that, this function can be able to set the state of different component instances. How could I do that?
Addition:
Please check the link below to see a gif image showing how my system works wrong. As you can see, even though I choose the third input box element to edit it's properties (in this case, set it's placeholder text), the change is being made to the first one.
Go to gif
Add a this. to the beginning, as in:
this.setLabel = setLabel.bind(this);
Now you're setting a property on the instance of the InputBox. Make sure to refer to it using this.setLabel when you reference it later in the component.
Is setLabel acting on a specific postID? Is the problem that <Button /> of every <ModalPanel /> acting on the same postID? Because you aren't using setLabel correctly inside <ModalPanel />. setLabel takes in 2 arguments and right now your implementation isn't using any. This is your click handler.
onClick = {() => props.setLabel()}
Try console.logging inside setLabel and see what values you're getting when you click on each button
function setLabel(postID, attributeName){
console.log(postID, attributeName)
var text = 'example';
if(text !== ''){
this.setState({ [attributeName] : text});
}
}
Since the React components only updated from props or state changes, you need to pair the global state with a local state to update the component. See the code below in a sandbox environment.
let value = 0;
function updateStuff () {
console.log("this from update", this.name);
value++;
this.setState({name: "Hakan " + value});
}
class Test extends React.Component {
constructor(props){
super(props);
this.state = {
name: 'notchanged',
counter: 1
}
this.localFunc = this.localFunc.bind(this)
updateStuff = updateStuff.bind(this)
}
localFunc(){
let {counter} = this.state;
this.setState({counter: counter + 1});
updateStuff();
}
render () {
return (
<div>
<div>Test 2</div>;
<div>Counter: {this.state.counter}</div>
<div>Name: {this.state.name}</div>
<button onClick={this.localFunc}>Increment</button>
</div>
);
}
}
ReactDOM.render(
<Test/>,
document.getElementById('root')
);
Think, you are using React in incorrect way
The preferred way for me looks like:
Have a dumb/presentational InputBox which accepts label as a property (in props, not in state)
Have a smart/container component which contains state of multiple InputBoxes and passes the correct label into InputBox
If you are trying to implement InputBox PropertyEditor as a separate component - consider adding event bus, shared between them for example via React Context (or even use full flux/redux concept)
Add this to your function calls after binding them or use arrow functions !
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
I use Jest to test my React Components. However, I have no idea (or haven't seen anything) how to test components that pass (as prop) on methods to sub components. For instance, I have: Form, MemberList, Member, FormButton. Something similar to this in code:
Form:
<MemberList members={this.state.members} remove={this.remove} add={this.add} />
<FormButton data={this.state.members} />
MemberList:
<span onClick={this.add}> <!-- add button --> </span>
{this.props.members.map(function(member, index) {
<Member key={index} data={member} remove={this.props.remove} />
})}
Member:
// some input like name and so, and a remove itself button.
FormButton:
var submit = function() {
this.setState({ loading: true });
// xhr
}
<button type="button" onClick={submit} disabled={this.state.loading}>Submit</button>
Am I thinking in the right mindset? To add, are there any practical examples out there?
*I've never tested before trying out React and Jest.
The solution is to pass a mocked function directly to the sub components and test them. Anything involving more than one "sub-component" is usually not truly a unit test as you are testing multiple units of functionality.
So I would create MemberList-test.js:
describe('MemberList', function () {
it('calls the add handler when add is clicked', function () {
var Component = TestUtils.renderIntoDocument(
<MemberList add={ jest.genMockFn() } />
);
const btn = TestUtils.findRenderedDOMComponentWithTag(Component, 'span')
TestUtils.Simulate.change(btn);
expect(Component.add.mock.calls.length).toBe(1)
})
})
Then rather than trying to test your member component directly within the same test your should create Member-test.js:
describe('Member', function () {
it('calls the add handler when add is clicked', function () {
var Component = TestUtils.renderIntoDocument(
<Member remove={ jest.genMockFn() } />
);
const btn = TestUtils.findRenderedDOMComponentWithTag(Component,
'HOWEVER YOU FIND YOUR REMOVE BUTTON')
TestUtils.Simulate.change(btn);
expect(Component.remove.mock.calls.length).toBe(1)
})
})
Now the assertion your are missing is that the remove handler that is passed into the member list is correctly getting passed down to the Member component. So let's add another test to the MemberList-test.js
it('passes correct information to the children', function () {
var MemberMock = require('../Member')
var removeFn = jest.genMockFn();
var testMember = {WHATEVER YOUR MEMBER OBJECT LOOKS LIKE}
var Component = TestUtils.renderIntoDocument(
<MemberList members={ [testMember] }
remove={ removeFn } />
);
// We expect the member component to be instantiated 1 time and
// passed the remove function we defined
// as well as a key and the data
expect(MemberMock.mock.calls).toEqual([[{key: 0, data: testMember,
remove: removeFn}]])
})
Then you simply do the same pattern with the form component. Mocking the member list and the button and testing them separately and seeing that the correct handlers and data are passed down.
Hopefully this makes sense and if not just reply and maybe I can walk you through it on Skype or something.
How can I find the vue.js component corresponding to a DOM element?
If I have
element = document.getElementById(id);
Is there a vue method equivalent to the jQuery
$(element)
Just by this (in your method in "methods"):
element = this.$el;
:)
The proper way to do with would be to use the v-el directive to give it a reference. Then you can do this.$$[reference].
Update for vue 2
In Vue 2 refs are used for both elements and components: http://vuejs.org/guide/migration.html#v-el-and-v-ref-replaced
In Vue.js 2 Inside a Vue Instance or Component:
Use this.$el to get the HTMLElement the instance/component was mounted to
From an HTMLElement:
Use .__vue__ from the HTMLElement
E.g. var vueInstance = document.getElementById('app').__vue__;
Having a VNode in a variable called vnode you can:
use vnode.elm to get the element that VNode was rendered to
use vnode.context to get the VueComponent instance that VNode's component was declared (this usually returns the parent component, but may surprise you when using slots.
use vnode.componentInstance to get the Actual VueComponent instance that VNode is about
Source, literally: vue/flow/vnode.js.
Runnable Demo:
Vue.config.productionTip = false; // disable developer version warning
console.log('-------------------')
Vue.component('my-component', {
template: `<input>`,
mounted: function() {
console.log('[my-component] is mounted at element:', this.$el);
}
});
Vue.directive('customdirective', {
bind: function (el, binding, vnode) {
console.log('[DIRECTIVE] My Element is:', vnode.elm);
console.log('[DIRECTIVE] My componentInstance is:', vnode.componentInstance);
console.log('[DIRECTIVE] My context is:', vnode.context);
// some properties, such as $el, may take an extra tick to be set, thus you need to...
Vue.nextTick(() => console.log('[DIRECTIVE][AFTER TICK] My context is:', vnode.context.$el))
}
})
new Vue({
el: '#app',
mounted: function() {
console.log('[ROOT] This Vue instance is mounted at element:', this.$el);
console.log('[ROOT] From the element to the Vue instance:', document.getElementById('app').__vue__);
console.log('[ROOT] Vue component instance of my-component:', document.querySelector('input').__vue__);
}
})
<script src="https://unpkg.com/vue#2.5.15/dist/vue.min.js"></script>
<h1>Open the browser's console</h1>
<div id="app">
<my-component v-customdirective=""></my-component>
</div>
If you're starting with a DOM element, check for a __vue__ property on that element. Any Vue View Models (components, VMs created by v-repeat usage) will have this property.
You can use the "Inspect Element" feature in your browsers developer console (at least in Firefox and Chrome) to view the DOM properties.
Hope that helps!
this.$el - points to the root element of the component
this.$refs.<ref name> + <div ref="<ref name>" ... - points to nested element
💡 use $el/$refs only after mounted() step of vue lifecycle
<template>
<div>
root element
<div ref="childElement">child element</div>
</div>
</template>
<script>
export default {
mounted() {
let rootElement = this.$el;
let childElement = this.$refs.childElement;
console.log(rootElement);
console.log(childElement);
}
}
</script>
<style scoped>
</style>
So I figured $0.__vue__ doesn't work very well with HOCs (high order components).
// ListItem.vue
<template>
<vm-product-item/>
<template>
From the template above, if you have ListItem component, that has ProductItem as it's root, and you try $0.__vue__ in console the result unexpectedly would be the ListItem instance.
Here I got a solution to select the lowest level component (ProductItem in this case).
Plugin
// DomNodeToComponent.js
export default {
install: (Vue, options) => {
Vue.mixin({
mounted () {
this.$el.__vueComponent__ = this
},
})
},
}
Install
import DomNodeToComponent from'./plugins/DomNodeToComponent/DomNodeToComponent'
Vue.use(DomNodeToComponent)
Use
In browser console click on dom element.
Type $0.__vueComponent__.
Do whatever you want with component. Access data. Do changes. Run exposed methods from e2e.
Bonus feature
If you want more, you can just use $0.__vue__.$parent. Meaning if 3 components share the same dom node, you'll have to write $0.__vue__.$parent.$parent to get the main component. This approach is less laconic, but gives better control.
Since v-ref is no longer a directive, but a special attribute, it can also be dynamically defined. This is especially useful in combination with v-for.
For example:
<ul>
<li v-for="(item, key) in items" v-on:click="play(item,$event)">
<a v-bind:ref="'key' + item.id" v-bind:href="item.url">
<!-- content -->
</a>
</li>
</ul>
and in Vue component you can use
var recordingModel = new Vue({
el:'#rec-container',
data:{
items:[]
},
methods:{
play:function(item,e){
// it contains the bound reference
console.log(this.$refs['key'+item.id]);
}
}
});
I found this snippet here. The idea is to go up the DOM node hierarchy until a __vue__ property is found.
function getVueFromElement(el) {
while (el) {
if (el.__vue__) {
return el.__vue__
} else {
el = el.parentNode
}
}
}
In Chrome:
Solution for Vue 3
I needed to create a navbar and collapse the menu item when clicked outside. I created a click listener on windows in mounted life cycle hook as follows
mounted() {
window.addEventListener('click', (e)=>{
if(e.target !== this.$el)
this.showChild = false;
})
}
You can also check if the element is child of this.$el. However, in my case the children were all links and this didn't matter much.
If you want listen an event (i.e OnClick) on an input with "demo" id, you can use:
new Vue({
el: '#demo',
data: {
n: 0
},
methods: {
onClick: function (e) {
console.log(e.target.tagName) // "A"
console.log(e.targetVM === this) // true
}
}
})
Exactly what Kamil said,
element = this.$el
But make sure you don't have fragment instances.
Since in Vue 2.0, no solution seems available, a clean solution that I found is to create a vue-id attribute, and also set it on the template. Then on created and beforeDestroy lifecycle these instances are updated on the global object.
Basically:
created: function() {
this._id = generateUid();
globalRepo[this._id] = this;
},
beforeDestroy: function() {
delete globalRepo[this._id]
},
data: function() {
return {
vueId: this._id
}
}