Getting React unique ID in didMount - input

I have a custom react Input component. I want all my inputs to have a small hint beside them (an asterix) that on hover shows a hint. The problem is I cannot make the initialization of the popup point to this exact asterix so it shows the message for this particular component. Right now it just replaces the message with the message of the last mounted component.
My question is - how would I reference the exact element. Can I get the React ID from didMount? Can I point to it using render, something like $(this.render() + ' i') -> (ideal case).
import React, { Component, PropTypes } from 'react'
export default class Input extends Component {
componentDidMount() {
var html = this.props.popup;
console.log(this);
$('.inverted.asterisk.icon').popup({
html: html,
variation: 'inverted'
});
}
render() {
return (
<div className="ui icon fluid input">
<input
type={this.props.type}
value={this.props.value}
onChange={this.props.onChange}
name={this.props.name}
/>
<i className="inverted disabled asterisk link icon" />
</div>
)
}
}
Input.propTypes = {
type: PropTypes.string,
name: PropTypes.string,
popup: PropTypes.string,
value: PropTypes.node,
onChange: PropTypes.func
}

You can assign a ref attribute to any element inside your render function to get a reference to it.
Assign the ref
<i ref="icon" className="inverted disabled asterisk link icon" />
Use it in componentDidMount
componentDidMount() {
var html = this.props.popup;
console.log(this);
$(this.refs.icon).popup({
html: html,
variation: 'inverted'
});
}
jsfiddle example

Related

Vue Testing Library with NaiveUI

I'm using Vue 3, NaiveUI, Vitest + Vue Testing Library and got to the issue with testing component toggle on button click and conditional rendering.
Component TSample:
<template>
<n-button role="test" #click="show = !show" text size="large" type="primary">
<div data-testid="visible" v-if="show">visible</div>
<div data-testid="hidden" v-else>hidden</div>
</n-button>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
import { NButton } from 'naive-ui'
export default defineComponent({
name: 'TSample',
components: {
NButton
},
setup() {
const show = ref(true)
return {
show
}
}
})
</script>
The test case I have:
import { render, waitFor } from '#testing-library/vue'
import TSample from './TSample.vue'
import userEvent from '#testing-library/user-event'
describe('Tests TSample component', () => {
it('toggles between visible and hidden text inside the button', async () => {
const user = userEvent.setup()
const { getByText, queryByText, getByRole } = render(TSample)
expect(getByRole('test')).toBeInTheDocument()
expect(getByText(/visible/i)).toBeInTheDocument()
expect(queryByText('hidden')).not.toBeInTheDocument()
await user.click(getByRole('test'))
await waitFor(() => expect(queryByText(/hidden/i)).toBeInTheDocument()) <-- fails
})
})
The error:
<transition-stub />
<span
class="n-button__content"
>
<div
data-testid="visible"
>
visible
</div>
</span>
</button>
</div>
</body>
</html>...Error: expect(received).toBeInTheDocument()
received value must be an HTMLElement or an SVGElement.
Moreover in Testing Preview I get:
<button
class="n-button n-button--primary-type n-button--large-type mx-4"
tabindex="0"
type="button"
disabled="false"
role="test"
>
<transition-stub>
</transition-stub><span class="n-button__content">
<div data-testid="visible">visible</div>
</span>
</button>
a button, which makes it more confusing to me... Same situation happened when I replaced waitFor with nextTick from Vue, the component didn't do a toggle at all on click.
What works but isn't acceptable
When I changed the n-button to just button, the test passed and divs are toggled, but that's not the goal of this component. The component isn't supposed to be changed.
What I have tried:
I tried different approaches with reaching the div that contains hidden text. Either it was like above - queryByText/getByText or getByTestId, but test fails at the same point.
Also followed with similar approach shown at Testing Library - Disappearance Sample
but doesn't work in my case above.
What actually is going on and how can I test the change on click with such third-party components?
If more info is needed/something is missing, also let me know, I'll update the question.
Any suggestions, explanations - much appreciated.

How to do conditional rendering on dynamic props

I'm trying to add conditional rendering on the dynamically passed prop. I have a component called card-item.vue which is passing the prop cta to component profile.vue.
Now in profile.vue I want the prop cta to display on every card-item component except the first one.
Here is my card-item.vue:
<template>
<span v-if="cta">
{{ cta }}
</span>
</template>
<script>
export default {
name: 'card-item',
props: {
cta: {
type: String
}
}
}
</script>
profile.vue:
<template>
<CardItem
v-for="address in addresses.slice(1)"
:key="uniqueKey('address', address)"
:cta="cms.page.cta" // remove cta from the first address only
/>
</template>
<script>
import CardItem from "./card-item";
const data = require('./profile.json');
export default {
name: 'profile',
comonents: {
CardItem,
},
props: {
cms: {
type: Object,
default: () => {
return {
page: data
}
}
}
}
}
</script>
profile.json:
{
"cta": "Report"
}
In my <CardItem /> component I'm rendering addresses. So I want my :cta on every address except the first one.
I was trying something like:
v-if="addresses[0] ? {cta="null"} : "cms.page.cta""
I know this is incorrect syntax but somewhat I'm trying to achieve.
Please any help would be appreciated.
v-for also supports an optional second argument for the index of the
current item. -- vue docs
<CardItem
v-for="(address, index) in addresses.slice(1)"
:key="uniqueKey('address', address)"
:cta="index !== 0 ? cms.page.cta : null" // remove cta from the first address only
/>

Why does a keydown event get broadcasted to a newly rendered component?

I want to conditionally render a component based on the user's keypress.
Once the new component is rendered, an input field should get focused.
For some reason, Vue broadcasts the keypress I use to render the component to the new component! The result is that the key I pressed to render the component gets displayed in the input field!
How is that even possible? The keypress triggers the mounting of the new component and the focusing of the input only happens after it is mounted.
Minimum working example:
// App.vue
<template>
<div>
<p>App</p>
<Hello v-if="view == 'hello'" />
</div>
</template>
<script>
import Hello from "./components/Hello.vue";
export default {
components: {
Hello
},
data() {
return {
view: null
};
},
methods: {
changeView() {
this.view = "hello";
}
},
created() {
document.addEventListener("keydown", this.changeView);
}
};
</script>
// Hello.vue
<template>
<div>
<p>Hello</p>
<input ref="input" type="text" />
</div>
</template>
<script>
export default {
mounted() {
this.$refs.input.focus();
}
};
</script>
This is because the events are flowing like this:
keydown renders the new component -> focus moved to new component -> keyup fires in new component (where is focus now) and character stays there.
Change the trigger to keyup and it should work.

Component Declaration And Communication

I have added a component declaration to the default main.js file which is generated during the Webpack project creation process as
import Modal from '#/components/Modal'
Vue.component('modal', Modal)
And in the App.vue, I have
<modal v-show="showModal"></modal>
<button id="show-modal" v-on:click="showModal = true">Click to have a modal</button>
And they work fine. Now, I need to setup a "props down, events up" communication channel between the parent and a child. To do so, I need to add a property, called 'isActive', the Modal component so that the root component can send a message to the child component, that is
<modal isActive="showModal"></modal>
<button id="show-modal" v-on:click="showModal = true">Click to have a modal</button>
I guess the component declaration should be something like:
Vue.component('modal', {
props: ['isActive'],
Modal
})
It doesn't work, however, due to
Failed to mount component: template or render function not defined.
I have tried different variants without a luck.
My second question is that how a child event changes its parent data. For example, in the child component
<button class="modal-close is-large" v-on:click="closeModal"></button>
the closeModal event is handled in the following javacript code in the child component.
export default {
method: {
closeModal: function(event) {
...
}
}
}
How can I set its parent data showModal to false?
Update:
The code segment of Modal:
<template>
<div class="signin">
<div class="modal" v-bind:class="{ 'is-active': isActive }">
...
</div>
<button class="modal-close is-large" v-on:click="isActive = false"></button>
</div>
</div>
</template>
<script>
import axios from 'axios'
import _ from 'lodash'
import Notification from '#/components/Notification'
import { required, email } from 'vuelidate/lib/validators'
export default {
name: 'signin',
components: {
Notification
},
data: () => ({
isActive: true,
email: '',
...
}),
...
}
</script>
Bulma is used for styling. And the isActive is defined in the Modal. I think it needs to be changed to achieve "props down".
As it looks, your file /components/Modal contains a full definition of a component: the template, and the script parts for it. So you can just bind the component to the tag-name you want to use in your markup:
import Modal from '#/components/Modal'
Vue.component('modal', Modal)
This is basically what you had in the beginning. To pass properties to this component, add the props-line directly to your component, that is into /components/Modal:
...
export default {
name: 'signin',
components: {
Notification
},
props: ['isActive'],
data: () => ({
...
As for the second question, how to communicate back to the parent, have a look at Vue's Custom Events. Basically, your Modal component could issue a "close"-event like this:
methods: {
closeModal: function(event) {
this.$emit('modalClose')
}
}
and when you use the component, you could listen to it like this:
<modal v-bind:isActive="showModal" v-on:modalClose="showModal = false"></modal>
Note that you should use v-bind for providing the value to isActive. If you don't use v-bind, the value is just passed once when the component is created. This means, the component would never see a change to this prop when it is changed by the parent. By using v-bind, changes by the parent to this attribute are pushed down to the child-component, so the Modal actually sees the updated value and can react to it.

REACT - defaultChecked don't render check attribute on second load

I got my component who won't check the radio when i go to the /view/:id for the second time. I started in my list component with react-router at the index of the site, i click on the view button of an element, the radio is checked, i return in my list and go to another or the same element and it's not checked anymore. When i inspect the component in the React developer tool, the radio has the defaultChecked=true property.
import React from 'react';
import { connect } from 'react-redux';
class LicenseRadios extends React.Component {
buildRadios() {
let { licenses, activeValue } = this.props;
return licenses.map(license => {
let checked = false;
if(activeValue !== undefined && activeValue === license.id){
checked = true;
}
return (
<div key={license.id} className="col l2">
<p>
<input name="license" type="radio" id={'licenseRdo_' + license.id} value={license.id} defaultChecked={checked} />
<label htmlFor={'licenseRdo_' + license.id}>{license.label}</label>
</p>
</div>
);
});
}
render() {
return (
<div className="row">
{this.buildRadios()}
</div>
);
}
}
export default LicenseRadios;
I tried to change the defaultChecked for the checked attribute, but it require an onChange event. I don't understand this problem. Can anybody help me please?
Thank you
The defaultChecked prop is only used during initial render. If you need to update the value in a subsequent render, you will need to use an onChange function to handle value changes.
Check out controlled components in the docs to better understand the problem you're having.
use "undefined" for initial value for defaultChecked and re-render by setting it to true or false
const Example = () => {
[checked,setChecked] = useState(undefined);
useEffect(()=>{
// fetch data
setChecked(true);
});
return (
<input type="checkbox" defaultChecked={checked} onClick={(e)=> changeValue(e)}/>
);
}