I'm trying to create new instances of the same component in svelte whenever a button or other action happens on the page, without having to make a list and {each} over them.
I just want to do something like new Component(some context data) and forget.
Another concern is I don't want the component to disappear when the parent is removed.
Thank you
Just mount it straight on the document.body:
new Component({ target: document.body })
But be aware that if you do not $destroy it yourself, you will end up with a memory leak.
When I do something like this, e.g. for notifications, I dispatch an event from the component that tells the calling code when the component can safely be destroyed. Something like:
const notification = new Notification({ target: document.body, ... });
notification.$on('close', () => notification.$destroy());
Related
In Svelte, I have a parent component which listens to a component event dispatched by a child component.
I know how to use component.$on to check that the dispatched event does the right thing within the component which is dispatching, like so.
But I can't figure out how to check that the component which receives the dispatch does the right thing in response.
Here's a basic example:
Child.svelte
<script>
import { createEventDispatcher } from 'svelte'
const dispatch = createEventDispatcher()
function handleSubmit(event) {
dispatch('results', 'some results')
}
</script>
<form on:submit|preventDefault={ handleSubmit }>
<button type='submit'>Submit</button>
</form>
Parent.svelte
<script>
import Child from './Child.svelte'
let showResults = false
function handleResults(event) {
showResults = true
}
</script>
<Child on:results={ handleResults } />
{ #if showResults }
<p id='results'>Some results.</p>
{ /if }
The idea is to eventually write a test using #testing-library/svelte like:
import { render } from '#testing-library/svelte'
import Parent from './Parent.svelte'
test('shows results when it receives them', () => {
const rendered = render(Parent)
// ***
// Simulate the `results` event from the child component?
// ***
// Check that the results appear.
})
If the parent were reacting to a DOM event, I would use fireEvent.
But I don't know how I would get a hold of the <Child> component in this case, and even if I could I'm guessing that Svelte is using a different mechanism for component events.
(Just to test it out, I used createEvent to fire a custom results event on one of the DOM elements rendered by <Child> but it didn't seem to do anything.)
Anyone have any ideas? Thanks!
If you're already planning on using #testing-library/svelte, I think the easiest way is not to try to manually trigger the Child component's results event, but to use Testing Library to grab the form/submit elements and trigger the submit event (using fireEvent a SubmitEvent on the <form> or their #testing-library/user-event library, or even a vanilla dispatchEvent). Svelte would then dispatch the custom results event that Parent is listening on.
Something like:
test('shows results when it receives them', async () => {
// Arrange
const rendered = render(Parent)
const submitButton = rendered.getByRole('button', {
name: /submit/i
});
const user = userEvent.setup();
// Act
await user.click(submitButton);
// Assert
const results = rendered.queryByText(/some results\./i);
expect(results).not.toBe(null);
});
Hope this is what you had in mind.
Edit:
For mocking Child.svelte, something like this in a __mocks__/Child.svelte should work:
<script>
import { createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher();
function handleSubmit(event) {
dispatch("results", "some results");
}
</script>
<form on:submit|preventDefault={handleSubmit}>
<button type="submit">Test</button>
</form>
Which is the exact same implementation as the actual module (I gave the button a different label just to make it clear it's the mocked version when querying it), but the idea is that this would never need to change and is only used to dispatch a results event. Then you'd just need to tell Jest or whatever you're using that you're mocking it (jest.mock("./Child.svelte");), change the getByRole query to match the new name (or just leave the mock with the original name), then it should just work.
Whether you think that's worth it or not is up to you. I've generally had success testing the UI as a whole rather than mocking sub-components, but I guess it comes down to preference. Yes, you might have to change the test if the Child component changes, but only if you change the label of the button or change the user interaction mechanism.
You don't need to know about the details of the components, you don't even need to know that it's split into a separate Child component, all the test would care about is a general idea of the structure of the UIāthat there's a button called "Submit" and that clicking on it should show an additional <p> tag.
Most examples for using RxJS to observe button clicks are like:
var button = document.querySelector('button');
Rx.Observable.fromEvent(button, 'click')
.subscribe(() => console.log('Clicked!'));
Is this okay in React? Since there is the virtual DOM, is it still okay to get reference to the real DOM like this? What happens after a re-render? Is the subscription lost?
I.e. they use document.querySelector.
But when I write my render() method, I'm used to <button onClick={...} >.
What is the recommended way to do this in React? Get rid of the onClick inside the JSX, add a class/id to my button, or maybe a ref, and use the style above?
Also, should I unsubscribe anywhere, e.g. in componentWillUnmount()? In which case I'd want to keep references to all my subscribers (event listeners)? In which case this seem much more complex than the old (callback) way.
Any suggestions, thoughts on this?
I've thought about this a lot - I think the answer by #ArtemDevAkk is one really good way to do it, but I'm not sure it accounts for how you intend to use the data. I'd like to suggest an alternative approach. Admittedly, I've used hooks for this, but you could do this in old school classes in a similar way.
I've had to break up your question into a few parts:
How to reference a DOM node in React
How to create a Subject and make it fire
How to subscribe to any Observable in React (useEffect)
Getting a reference to a DOM node
In case all you are asking is how to reference a DOM node, the rule of thumb is that to access the DOM in React, you use the useRef hook (or createRef in a Class).
Keeping your Subject active (for sharing events)
The benefit of this method is that your Subject will be created once and kept alive indefinitely, so anything is able to subscribe and unsubscribe at will. I can't think of a great reason for using RxJs in a React project because React has its own ways to handle events, so it's hard to know if this will solve your problem.
function MyRxJsComponent() {
// establish a stateful subject, so it lives as long as your component does
const [ clicks$ ] = useState(new Subject());
// register an event handler on your button click that calls next on your Subject
return <button onClick={e => clicks$.next(e)}></button>
}
This will create one Subject that will stay alive as long as your component is alive on the page. My best guess for how you might use this is by using a Context to contain your subject (put clicks$ in your context) and then pushing events to that Subject.
Note how I'm not using fromEvent to create an Observable, because this Observable would be created and destroyed with your button, making it impossible for anything to stay subscribed to it outside the component.
Just using an Observable internally
Alternatively (as #ArtemDevAkk alluded to), you could just create your Observable for local use and accept the limitation that it will be created and destroyed (this might actually be preferable, but again, it depends on what you're actually trying to achieve).
function MyRxJsComponent() {
// get a reference to the button (use <button ref={buttonRef} in your component)
const buttonRef = useRef(null)
// use useEffect to tell React that something outside of its
// control is going to happen (note how the return is _another_
// function that unsubscribes.
useEffect( () => {
// create the observable from the buttonRef we created
const clicks$ = fromEvent(buttonRef.current, 'click')
clicks$.subscribe( click => {
// do whatever you need to with the click event
console.log('button was clicked!', click)
})
return () => {
start$.unsubscribe()
}
}, [buttonRef]);
return <button ref={buttonRef}>Click me!</button>;
}
But should you?
I think overall, I've shown above how you can use state to keep a Subject alive for as long as you need; you can use useEffect() to subscribe to an observable and unsubscribe when you don't need it any more. (I haven't tested the samples locally, so there might be a small tweak needed..)
I mentioned using a Context to share your Subject with other components, which is one way I can imagine RxJs being useful in React.
Ultimately, though, if this is all you want to do, there are better ways to handle events in React. A simple <button onClick={() => console.log('clicked')}> could be all you need. React, being a reactive library in itself, isn't really designed to contain the amount of state that an Observable can contain - your views are meant to be simplified projections of state stored elsewhere. For the same token, it's generally advised that you don't try to reference a specific element unless you have an exceptional reason to do so.
const buttonRef = useRef(null)
useEffect( () => {
const start$ = fromEvent(buttonRef.current, 'click').subscribe( click => {
console.log('click event :', click)
})
return () => {
start$.unsubscribe()
}
}, [])
add ref to button
I'm wondering if its possible to emit events from child component and listen in parent without using the convenient #event{eventMame} syntax. I'm using a plugin that introduces this problem.
I'm using the vuetable2 component and to attach actions to rows, I have to do something like the following:
{
name: '__component:custom-actions',
title: 'Actions',
titleClass: 'text-center',
dataClass: 'text-center'
}
I haven't been able / don't know how to attach an event listener using the # to the custom-actions component.
I've tried the following in the created method:
created = () => {
this.$on('eventName', this.methodName');
}
but that doesn't seem to work.
In the custom-actions component I'm doing:
this.$emit('eventName');
Do not use a fat arrow to define the created hook. this will point to the window, not Vue, and both this.methodName and this.$on will be undefined. Use
created(){
this.$on('eventName', this.methodName');
}
or
created: function(){
this.$on('eventName', this.methodName');
}
Post Comment Edit
You can use a bus to communicate through multiple layers of components. Outside of your components, declare a bus.
const bus = new Vue();
In your nested component
bus.$emit('eventName');
In your parent
created(){
bus.$on('eventName', this.methodName');
}
I have a component like that:
<Parent>
<Child/>
</Parent>
and <Child/> component have a method foo. I want test the foo method but I don't know how to access it. I tried:
mount(<Parent><Child/></Parent>).props().children.foo
or
mount(<Parent><Child/></Parent>).children().foo
but both them are undefined. I can't use .instance() because it's not root. I can't mount <Child/> only because the <Parent> add something (react-router's context.router) on context and I need them when init <Child/>. Any idea with this?
This worked for me:
mount(<Parent><Child/></Parent>).find(Child).instance().foo
I would consider writing tests for only your parent class, and then a separate test file to only test your child.
Once you have mounted you component using:
const component = mount(<Child>);
you then have access to it's methods using:
component.instance().methodname
You can then do stuff like override it with jest.fn() and test appropriately.
I prefer shallow mount over full mount from enzyme.
In conjunction with proxyquire to resolve child component (which you want to test)
I do
wrapper.find('Child1').props().propName
And test it.
Or I use shallow
mount wrapper.dive()
I think your problem is way different from how to test child components.
My first question is: Why are you checking if a child component has a specific method in the parent's component tests?
IMHO you need to have a test specific for this component and, then, in this test you check if the method exists.
Just to not leave without the answer, did you tried .find(Child).instance().foo ?
I had a similar problem when trying to mock a function on an inner component within a MemoryRouter:
cont wrapper = mount(<MemoryRouter><AvailabilityButtonWithRouter.WrappedComponent vetId={ vetId } appointment={ availability } /></MemoryRouter>);
I ended up being able to mock the function like so:
const mockFn = jest.fn();
wrapper.children(0).children(0).instance().reloadCurrentPage = mockFn;
I was able to get a handle on child function like the following, i was looking for the first child to call the function on -
const component = shallow(<Component />);
component.find(Child).first().getNode().props.someChildFunction()
I faced a similar problem and I went through mount API by logging. In my use case, my child component(CommonStoresReactions) is wrapped with mobx inject.
const jsx = (
<Provider {...stores}>
<CommonStoresReactions>
<div />
</CommonStoresReactions>
</Provider>
)
const wrapper = mount(jsx)
I want to test clearStores method in CommonStoresReactions. Below snippet worked for me.
wrapper
.find(CommonStoresReactions)
.instance()
.wrappedInstance.clearStores()
Enzyme has an option for the mount API called wrappingComponent (and wrappingComponentProps) to wrap the mounted object inside another component for providing context, etc.
See https://github.com/airbnb/enzyme/blob/master/docs/api/mount.md#mountnode-options--reactwrapper
I managed to solve this by using dive
wrapper.dive().props().propName
With enzyme:
mount(<Parent><Child/></Parent>).childAt(0).instance().foo
There are valid reasons to access the child and call a method. If the parent is superficial and children have a consistent interface you can call methods without knowing which child it is, testing that all children have the correct interface, signature etc.
The best way I find out is using shallow wrapper's dive method. Here is the doc: enzyme dive doc
Remember if ur parent component use the fully rendering like mount, then the react wrapper itself doesnt have the dive method so u have to use shallow render.
Here is one example:
let instance, child, diveChild;
describe('Test Parent child Child component', () => {
beforeEach(() => {
wrapper = shallow(<Parent {...props} />);
child = wrapper.find('Child');
diveChild = child.dive();
console.log(diveChild.instance());
});
test('Child get mounted', () => {
expect(child.exists()).toBeTruthy();
expect(child.debug()).toMatchSnapshot();
});
});
I would start by echoing #rnmalone's answer that you probably don't want to test a function on a child directly. That's not really unit testing, that's integration testing. You don't want to test your foo method.
That said, you may want to grab children to test their bar value and see if they received something you did to them by manipulating the parent. And since, if you mount at least (?), the children are fully instantiated, there's no reason to spy on a shim; you can go straight to the "real" child and test it.
Here's a test that does both. We do test a foo on a child, so to speak -- it's an event handler -- and then we test its bar value -- in this case that the proper child had a value set to match what we raised in the foo event.
Testing if foo sets bar
In this test, we've spun up a component we're testing (we were using Preact instead of React and htm in place of JSX; apologies if I don't clean this perfectly):
var wrapper = Enzyme.mount(
<MyParentComponent
myItemTypes={arrayTypes}
mySelectedItem={someSelectedItem}
onTabClicked={mySpy.onTabClicked}
/>
);
Now MyParentComponent has child components in its markup called MyChildComponent.
(This is its "live" code, returned by a functional component, and is not from a test file, to be overly clear.)
return <ul>
{Object.keys(props.myItemTypes).map(function (key) {
var isSelected = myItemTypes[key] === mySelectedItem;
return
<MyChildComponent
isSelected={isSelected}
tabType={myItemTypes[key]}
onTabClicked={props.onTabClicked}
></MyChildComponent>
;
})}
</ul>;
So the parent component is, with respect to the onTablClicked event handler, basically just a passthrough.
Now I can spoof a click on a child item like this using ReactTestUtils:
var specificItem = wrapper
.find('MyChildComponent')
.findWhere((x) => x.props().tabType.value === newlySelectedTab.value);
if (specificItem) {
var onTabClicked = lessonsTab.props().onTabClicked;
TestUtils.act(function () {
onTabClicked(newlySelectedTab);
});
}
wrapper.update();
The nasty part here is that I registered that onTabClicked from this on the parent component:
onTabClicked={mySpy.onTabClicked}
That is, that act on the selected child will just call my spy, and my spy does this:
spyOn(mySpy, 'onTabClicked').and.callFake(function (tab) {
wrapper.setProps({ mySelectedItem: tab });
});
That's problematic. We'll discuss that later.
Now I can run a test to see if the child's prop was updated.
it('should send the new selection value to the child component', function () {
var allItems = wrapper.find(MyChildComponent);
var selectedItem = navItems.findWhere((x) => x.props().isSelected);
expect(selectedItem.props().settingsTab.value).toBe(newlySelectedTab.value);
});
But that really reduces to mock foo and real bar
Again, the weird part of doing that is the fake click on the child is really close to testing your mocks, which you shouldn't really do. (Reference is a powerpoint. Click at your own risk.)
I could've just tested initial state setting like this:
it('should send the new selection value to the child component', function () {
// Act
wrapper.setProps({ mySelectedItem: itemToSelect });
wrapper.update();
// Assert
var allItems = wrapper.find(MyChildComponent);
var selectedItem = navItems.findWhere((x) => x.props().isSelected);
expect(selectedItem.props().tabType.value).toBe(itemToSelect.value);
});
... and that's probably good enough for whatever work you're doing. It reduces to nearly the same thing.
I guess the bonus is that you know the event handler is registered on the child, and that's something. Though we should probably just test that foo fired using mySpy on MyParentComponent, right? And then add a second test to see that the child value changes when the parent prop does.
So likely still a code smell somewhere.
I am using react, react-router & redux. The structure of my app is such:
CoreLayout
-> <MaterialToolbar /> (contains back button)
-> {children} (react-router)
When the user presses the back button, which is normally handled by the CoreLayout, I would like the current child component to handle the back button instead of the parent. (In my case, I would like the current view to check if its data has been modified, and pop up an 'Are you sure you wish to cancel?' box before actually going back.) If the child does not wish to handle this, the parent will do it's thing.
Another example would be allowing a childview to set the title in the toolbar.
My reading has told me that accessing a component through a ref and calling a method on it is not the react way -- this is also made a bit more difficult since I am using redux-connect. What is the correct way to implement this behavior?
This is how I would do it, assuming you mean your navigation back button (and not the browser back button):
class CoreLayout extends Component {
handleBack () {
//... use router to go back
}
render () {
return <div>
<MaterialToolbar />
{React.children.map(this.props.children, child => React.cloneElement(child, { onBack: this.handleBack }))}
</div>
}
}
class Child extends Component {
handleBackButtonClick () {
// Here perform the logic to decide what to do
if (dataHasBeenModifiedAndConfirmed) {
// Yes, user wants to go back, call function passed by the parent
this.props.onBack()
} else {
// User didn't confirm, decide what to do
}
}
render () {
return <div onClick={this.handleBackButtonClick.bind(this)}>
Go Back
</div>
}
}
You simply pass a function from the parent to the child via props. Then in the child you can implement the logic to check if you really want to delegate the work to the parent component.
Since you use react-router and your children are passed to your parent component through this.props.children, to pass the onBack function you need to map the children and use React.cloneElement to pass your props (see this answer if you need more details on that: React.cloneElement: pass new children or copy props.children?).
Edit:
Since it seems you want to let the children decide, you can do it this way (using refs):
class CoreLayout extends Component {
constructor () {
super()
this.childRefs = {};
}
handleBack () {
for (let refKey in Object.keys(this.childRefs) {
const refCmp = this.childRefs[refKey];
// You can also pass extra args to refCmp.shouldGoBack if you need to
if (typeof refCmp.shouldGoBack === 'function' && !refCmp.shouldGoBack()) {
return false;
}
}
// No child requested to handle the back button, continue here...
}
render () {
return <div>
<MaterialToolbar />
{React.children.map(this.props.children, (child, n) => React.cloneElement(child, {
ref: cmp => { this.childRefs[n] = cmp; }
}))}
</div>
}
}
class Child extends Component {
shouldGoBack () {
// Return true/false if you do/don't want to actually go back
return true
}
render () {
return <div>
Some content here
</div>
}
}
This is a bit more convoluted as normally with React it's easier/more idiomatic to have a "smart" parent that decides based on the state, but given your specific case (back button in the parent and the logic in the children) and without reimplementing a few other things, I think using refs this way is fine.
Alternatively (with Redux) as the other answer suggested, you would need to set something in the Redux state from the children that you can use in the parent to decide what to do.
Hope it's helpful.
I don't think there is a correct way to solve this problem, but there are many ways. If I understand your problem correctly, most of the time the back button onClick handler will be handled within CoreLayout, but when a particular child is rendered that child will handle the onClick event. This is an interesting problem, because the ability to change the functionality of the back button needs to be globally available, or at very least available in CoreLayout and the particular child component.
I have not used redux, but I have used Fluxible and am familar with the Flux architecture and the pub/sub pattern.
Perhaps you can utilize your redux store to determine the functionality of your back button. And your CoreLayout component would handle rendering the prompt. There is a bug with the following code, but I thought I would not delete my answer for the sake of giving you an idea of what I am talking about and hopefully the following code does that. You would need to think through the logic to get this working correctly, but the idea is there. Use the store to determine what the back button will do.
//Core Layout
componentDidMount() {
store.subscribe(() => {
const state = store.getState();
// backFunction is a string correlating to name of function in Core Layout Component
if(state.backFunction) {
// lets assume backFunction is 'showModal'. Execute this.showModal()
// and let it handle the rest.
this[state.backFunction]();
// set function to false so its not called everytime the store updates.
store.dispatch({ type: 'UPDATE_BACK_FUNCTION', data: false})
}
})
}
showModal() {
// update state, show modal in Core Layout
if(userWantsToGoBack) {
this.onBack();
// update store backFunction to be the default onBack
store.dispatch({ type: 'UPDATE_BACK_FUNCTION', data: 'onBack'})
// if they don't want to go back, hide the modal
} else {
// hide modal
}
}
onBack() {
// handle going back when modal doesn't need to be shown
}
The next step is to update your store when the child component mounts
// Child component
componentDidMount(){
// update backFunction so when back button is clicked the appropriate function will be called from CoreLayout
store.dispatch({ type: 'UPDATE_BACK_FUNCTION', data: 'showModal'});
}
This way you don't need to worry about passing any function to your child component you let the state of the store determine which function CoreLayout will call.