Cannot queryselect a shadowRoot on button click(for toggling view) - javascript

I have a parent component ha-config-user-picker.js and child component edit-user-view.js.
Parent component:
has a mapping of users and also has the child component tag with its props.
the click event gets the shadowRoot of the child component and invokes method toggleView.
<template is="dom-repeat" items="[[users]]" as="user">
<paper-button on-click="clickEditUser">
<paper-item>
...
</paper-item>
</paper-button>
</template>
<edit-user-view hass="[[hass]]" user="[[user]]"></edit-user-view>
clickEditUser(ev) {
this.user = ev.model.user;
const el = this.shadowRoot.querySelector("edit-user-view");
el.toggleView();
}
Child Component:
<fullscreen-pop-up>
<dialog-header title="Edit User"></dialog-header>
<div class="content">
...
</div>
</fullscreen-pop-up>
toggleView = () => {
const popup = this.shadowRoot.querySelector("fullscreen-pop-up");
const dialog = popup.shadowRoot.querySelector("paper-dialog");
dialog.toggle();
}
Error:
So when I click on a mapped user. First I get error Uncaught TypeError: Cannot read property 'shadowRoot' of null. which is const popup = this.shadowRoot.querySelector("fullscreen-pop-up");. So the popup returns null.
But if i click any user again. it gets the shadowRoot and works fine.
Question: So why is it null the first time and works after that?
and how do I solve this?
Can this might be the problem?
https://github.com/Polymer/polymer/issues/5144
Let me know if you need something more to understand. :)

The toggleView method is not executed in the context of your child component, you can try to console.log(this) before using this and you will see it's not the element.
You can try something like replacing:
el.toggleView();
with
el.toggleView.call(el);
to see if this is the issue and if so refactor your code however you see fit. Please note that I did not test this at this moment, so there might be small differences.

Related

How can I test a modal inside of a React Portal using React Testing Library?

I'm trying to test my Modal component with React Testing Library. The Modal is rendered inside of a React Portal. Now when I try to match my Modal with a snapshot the snapshot gets rendered as an empty div.
test('The component should render in body when open', () => {
const {container} = render(
<Modal>
<div>My dialog content</div>
</Modal>
);
expect(container).toMatchSnapshot();
});
The snapshot I get looks like this:
exports[`The component should render in body when open 1`] = `<div />`;
I've seen a few workarounds for instance passing {container: document.body} as a second argument to the render() function. But nothing really worked.
Also I cannot query for any elements via container. It always returns null.
const dialog = container.querySelector(".modal");
console.log(dialog); // null
First you should identify the container element of your portal, that "root" node that is added within the body to hold the portal content. Then you should manually append/add it in the DOM so that your render method knows where to render your Modal component. Then you should use the baseElement property instead of the container, since the portal element is rendered within the body and then test the firstChild of your baseElement (which is the actual portal element) against the snapshot.
This worked for me:
it('should render', () => {
document.body.innerHTML = '<div id="my-root-element-id" />';
const { baseElement } = render(<Default />);
expect(baseElement.firstChild).toMatchSnapshot();
});
I eventually made it work by snapshotting baseElement instead of container Both are returned properties by the render() function.
test('The component should render in body when open', () => {
const {baseElement} = render(
<Modal>
<div>My dialog content</div>
</Modal>
);
expect(baseElement).toMatchSnapshot();
});

How to verify modal window is displayed or not

I'm a newbie of Vuejs.
In vue2, I already have verified whether component is displayed by specific trigger.
However, I cannot verify whether above displayed component is disappeared after another trigger.
I tried to verify wrapper.findComponent('...').exists() is false.
However, I got true when component should be disappeared.
At first, I suspect another trigger does not work well,
so I called wrapper.html(), and there is no component that I want to verify.
That's why, trigger works well, probably.
My question is, as I said at title, How to verify component existance when flag is toggled.
Below is my code.
code for test.
test('After date pick, picker is closed', async() => {
let node = document.createElement('body')
const wrapper = mount(App, {
localVue,
vuetify,
attachTo: node
})
// modal window does not appear yet.
expect(wrapper.findComponent('.v-picker').exists()).toBe(false)
// `enable` is set to true. Then, modal window is displayed.
// and verification got true
const menu = wrapper.getComponent(DateMenu)
await menu.setData({ enable: true })
expect(wrapper.findComponent('.v-picker').exists()).toBe(true)
// $emit input event cause `enable` set to false
const picker = wrapper.getComponent('.v-picker')
await picker.vm.$emit('input', '2020-01-01')
// html() returns result in which there is no elements related to toggled component
console.log(wrapper.html())
expect(menu.vm.$data.enable).toBe(false)
// test fail findComponent found a component
expect(wrapper.findComponent('.v-picker').exists()).toBe(false)
})
As reference, source code (short version).
<template>
<!-- v-model="enable" controls modal window is displayed or not-->
<v-menu v-model="enable">
<template v-slot:activator="{ on, attrs }">
<v-text-field
v-model="date"
v-bind="attrs"
v-on="on"
>
</v-text-field>
</template>
<!-- This is displayed and verified existance in test. -->
<v-date-picker v-model="date" #input="input"> </v-date-picker>
</v-menu>
</template>
<script>
export default {
data: () => {
return {
// enable controls modal window is displayed or not.
// true: displayed
// false: disappear
enable: false,
};
},
methods: {
input(date) {
this.enable = false
}
}
};
</script>
I think in your test, you encounter an update applied by Vue, which is asynchronous behavior.
So after changing the reactive property (enable = false), you should wait for Vue to update DOM by adding this line:
await Vue.nextTick();
expect(wrapper.findComponent('.v-picker').exists()).toBe(false);
Vuetify renders its dialogs, menus, pickers (etc.) outside of the application wrapper, that's why wrapper.html() doesn't help you.
I believe the correct way is to use attachTo option, so you can mount your component inside the div with the data-app attribute.
If that doesn't work for you, you can also set data-app attribute on body:
let node = document.createElement('body');
node.setAttribute("data-app", true);

Svelte component created with JavaScript not receiving reactive updates

I dynamically create a component on button click with the following JS:
<script>
export let order;
function toggleModal(an_order){
modal_open.set(!$modal_open);
const modal = new OrderModal({
target: document.querySelector('.modal_container-' + order.id),
props: {
order: an_order
},
});
}
</script>
However whenever I update the order object it does not reactively reflect in the created component.
If I put the component directly in the html instead of making it dynamic like this:
<div class="modal_container-{order.id} fixed">
<OrderModal {order} />
</div>
<div class="max-w-xs w-full overflow-hidden rounded-lg shadow-md bg-white cursor-pointer" on:click="{() => toggleModal(order)}">
Then it works correctly and the elements are reactively updated.
Is there a way to make components created by JavaScript update reactively?
*** Updating to show how it is inserted ***
The function that does the update:
function deleteItem(){
order.order_items.splice(index, 1);
$order_list = $order_list;
}
As you can see I explicitly do an assignment to trigger the update which as specified works when the component is not created through javascript.
I don't know what's the context of your update function and where it's located/called from, but when creating a component programmatically/imperatively, triggering a rerender by assignment doesn't work. Instead you need to use the $set method on the component instance (docs: https://svelte.dev/docs#$set):
<script>
export let order;
let modal; // <- top level variable so it can be used in the update function
function toggleModal(an_order){
modal_open.set(!$modal_open);
modal = new OrderModal({
target: document.querySelector('.modal_container-' + order.id),
props: {
order: an_order
},
});
}
// ...
function deleteItem(){
order.order_items.splice(index, 1);
// imperatively update the order
modal.$set({order: $order_list});
}
// if you want updates of the `order` prop to propagate to the modal once it's set, you can do
$: modal && modal.$set({order: order});
</script>
The reason this doesn't work is because the compiler will create all the links between reactive elements during compilation. Your Modal component does not exist at that time so it is not possible to do so. This also explains why adding the Modal directly works.
One way to work around this is by using a store and pass this store into the modal.
In general in Svelte, you will not use the code as you have it. A more Svelte way to do it would be to use a bool to track if the Modal has to be shown or not:
<script>
let showModal = false
function toggleModal() {
showModal = !showModal
}
</script>
{#if showModal}
<Modal />
{/if}

"Warning: react-modal: App element is not defined. Please use `Modal.setAppElement(el)` or set `appElement={el}`"

How do I fix this warning in console of a React app using the react-modal package:
Warning: react-modal: App element is not defined. Please use Modal.setAppElement(el) or set appElement={el}
I have not been successful at figuring out what el is supposed to be.
Context:
in my App.js root component file:
...
import Modal from 'react-modal';
...
class App extends Component {
...
render(){
...
<Modal
className="modal"
overlayClassName="overlay"
isOpen={foodModalOpen}
onRequestClose={this.closeFoodModal}
contentLabel="Modal"
>
...
}
}
Where ... indicates code not shown.
Everything works fine, but when the Modal is opened, the following Warning appears in my console:
index.js:2177 Warning: react-modal: App element is not defined. Please use Modal.setAppElement(el) or set appElement={el}. This is needed so screen readers don't see main content when modal is opened. It is not recommended, but you can opt-out by setting ariaHideApp={false}.
In the react-modal docs all I can find is the following:
App Element
The app element allows you to specify the portion of your app that should be hidden (via aria-hidden) to prevent assistive technologies such as screenreaders from reading content outside of the content of your modal.
If you are doing server-side rendering, you should use this property.
It can be specified in the following ways:
DOMElement
Modal.setAppElement(appElement);
query selector - uses the first element found if you pass in a class.
Modal.setAppElement('#your-app-element');
Unfortunately, this has not helped! I cannot figure out what el is supposed to represent.
Here are some of the many property variations I have tried adding to my Modal component:
`appElement={el}`,
`appElement="root"` where `root` is the id that my App component is injected into
`appElement={'root'}`
`appElement="div"`,
`appElement={<div>}`,
`appElement={"div"}`
I've also tried calling Modal.setAppElement('root'); from inside src/index.js, where root is the root element that my App component is injected into, and index.js is where I do that.
Add ariaHideApp={false} to Modal attributes.
This should work:
<Modal isOpen={!!props.selectedOption}
onRequestClose={props.clearSelectedOption}
ariaHideApp={false}
contentLabel="Selected Option"
>
</Modal>
Some solutions are given in react-modal issue #133:
The problem lies here:
Depending on when it evaluates react-modal#1.6.5:/lib/helpers/ariaAppHider.js#L1:
document.body does not exist yet and it will resolve to undefined || null.
if Modal.setAppElement() is called with null or not called at all with the <script /> placed on <head /> (same as above).
Probably it can also happen if called with a selector that does not match any results.
Solutions:
Browser Rendering:
#yachaka snippet prevents this behavior by defining the element before placing the <Modal />:
componentWillMount() {
Modal.setAppElement('body');
}
#ungoldman answer, if you don't want to depend on `setAppElement':
Inject the bundled application JS into <body> instead of <head>.
Though ideally react-modal should wait until the DOM is loaded to try attaching to document.body.
server-side:
If rendering on server-side, you must provide a document.body, before requiring the modal script (perhaps it should be preferable to use setAppElement() in this case).
Update:
react docs have been updated to include the information above, so they should now be clearer for users running into this issue.
react-modal issue #567: add information (from issue #133 linked above) to the docs.
Just include appElement={document.getElementById('app')} inside your modal like this
<Modal
className="modal"
appElement={document.getElementById('app')}
>
It will work 100% if app is your central in index.html from where react loads.
This is my TypeScript Modal component which wraps react-modal v3.8.1:
import React from 'react'
import ReactModal from 'react-modal'
interface Props {
isOpen: boolean
ariaLabel?: string
}
const Modal: React.FC<Props> = ({
children,
ariaLabel = 'Alert Modal',
isOpen,
}) => (
<ReactModal
appElement={document.getElementById('root') as HTMLElement}
ariaHideApp={process.env.NODE_ENV !== 'test'}
isOpen={isOpen}
contentLabel={ariaLabel}
testId="modal-content"
>
{children}
</ReactModal>
)
export default Modal
Usage in component with state = { isOpen: true }:
<Modal isOpen={this.state.isOpen}>
<p>
Modal Content hereā€¦
</p>
<button onClick={() => { this.setState({ isOpen: false }) }}>Okay</button>
</Modal>
If getting the Warning: react-modal: App element is not defined... error when running tests (we were running Jest), you can suppress the warnings by adding the following to your test file:
import ReactModal from 'react-modal';
ReactModal.setAppElement('*'); // suppresses modal-related test warnings.
The shortest solution is to add
appElement={document.getElementById("hereIsYourRootElementId")}
It lets react-modal know where is your root element.
For reference, since it was a pain for me, if you are doing SSR, use the following code to prevent errors server-side:
if (typeof(window) !== 'undefined') {
ReactModal.setAppElement('body')
}
You could put this in componentDidMount() anywhere you use a modal or I put it in a custom modal component so it's nice and DRY.
Just put this
Modal.setAppElement('#root')
This will solve the warning. The root element coming from inside public folder index.html.
you need to add # before your root element id.
import React from 'react';
import Modal from 'react-modal';
Modal.setAppElement('#root');
const OptionModal = (props) => (
<Modal
isOpen={!!props.selectedOption}
contentLabel='this is the selected option'
>
<h3>Selected Option</h3>
{props.selectedOption && <p>{props.selectedOption}</p>}
<button onClick = {props.handleCloseOptionModal}>Close</button>
</Modal>
);
export default OptionModal;
here is the reference:
http://reactcommunity.org/react-modal/accessibility/
If you get that warning on testing with the "react-testing-library" here is a solution:
https://github.com/reactjs/react-modal/issues/576#issuecomment-524644035
using the react-testing-library (https://testing-library.com/) I get rid of that warning with:
import Modal from "react-modal";
const { container } = render(<MyComponent />);
Modal.setAppElement(container);
.... // to the testing, use Modal
or, if you want to test the modal component directly:
const { container, rerender } render(<MyModalComponent isOpen={false} />);
Modal.setAppElement(container);
// now the appElement is set we can show the modal component
rerender(<MyModalComponent isOpen={false} />);
.... // to the testing
For Nextjs, I think you can solve this by adding the below to outside your modal component, maybe on top, before the component is declared.
Modal.setAppElement('#__next')
Delete this attrib
className="modal"
and run again

Creating a global modal using react-redux and foundation reveal

I'm creating a modal that displays information about a specific user and it's triggered by clicking on a picture. The modal should be able to fire up from different components. My approach is as follows (I use React with Redux):
create store with a "modal" property that's set to a reducer "modalReducer"
modal: modalReducer
modal is an object with the following properties: showModal=false, user=undefined, image=undefined
When a div containing picture is clicked, the action is being dispatched that sets showModal to true (with username and url image link as arguments).
export var openModal = (user, image) => {
return {
type: 'OPEN_MODAL',
user,
image
}
}
For the actual modal I use a foundation reveal modal and in its render method I check if modal.showModal == true and use jQuery to open it up.
Now, toggling the modal works as charm but I cannot use the modal.user and modal.image properties inside of the render method as it prints the following error message:
Uncaught Error: findComponentRoot(..., .0.2.0.0.1): Unable to find element. This probably means the DOM was unexpectedly mutated (e.g., by the browser), usually due to forgetting a when using tables, nesting tags like , , or , or using non-SVG elements in an parent. Try inspecting the child nodes of the element with React ID ``.
I'm almost certain the problem lies within the ProfileModal.jsx component. Any idea what I'm doing wrong?
export class ProfileModal extends React.Component {
componentDidMount () {
var elem = new Foundation.Reveal($('#profile-modal'));
$('#profile-modal').on('closed.zf.reveal',()=>{this.props.dispatch(actions.hideModal())});
}
render () {
var {modal} = this.props;
if (modal.showModal) {
$('#profile-modal').foundation('open');
}
return(
<div>
<div id="profile-modal" className="reveal large">
<div className="modal-container">
<div className="modal-picture" style={{backgroundImage: `url(${modal.image})`}}></div>
<p>{modal.user}</p>
...

Categories

Resources