vue render components on click - vue.js

I have a component called customer-type.
I do have the full year and I do have a lot of components as well. So I want to reduce rendering.
How can I load a component after click renderComponent?
<template v-for="(date, index) in daysOfYear">
<b-tr :key="date" :id="`row-${getDay(date)}`">
<customer-type :id="`customer-${index}`" #test="setCustomer" v-bind:listTypes="listTypes" />
<button #click="renderComponent(index)"> </button>
</b-tr>
</template>
methods: {
renderComponent(index) {
}
}
I don't want to render the component before I explicitly click on it.

You can modify the daysOfYear to be a list of objects, each having a boolean to show/hide its customer-type component using v-if.
Here is a simple demo:
const customertype = Vue.component('customertype', { template: '#customertype', props: ['id'] });
new Vue({
el:"#app",
components: { customertype },
data: () => ({
daysOfYear: ['01-01-2021','01-02-2021','01-03-2021']
}),
created() {
this.daysOfYear = this.daysOfYear.map(date => ({ date, showCustomerType:false }));
},
methods: {
renderComponent(index) {
this.daysOfYear[index].showCustomerType = true;
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<template id="customertype"><p>{{id}}</p></template>
<div id="app">
<div v-for="({ date, showCustomerType }, index) in daysOfYear" :key="index">
<button #click="renderComponent(index)">Show</button>
<customertype
v-if="showCustomerType"
:id="`customer-${index}`"
/>
</div>
</div>

#Majed is right. Basically, v-if will only render the element in the DOM when the condition is met. Another alternative is v-show basically it works the same way as v-if, the difference is that v-show will always render the element in the DOM and v-if won't

Related

How do have unique variables for each dynamically created buttons/text fields?

I'm trying to create buttons and vue element inputs for each item on the page. I'm iterating through the items and rendering them with v-for and so I decided to expand on that and do it for both the rest as well. The problem i'm having is that I need to to bind textInput as well as displayTextbox to each one and i'm not sure how to achieve that.
currently all the input text in the el-inputs are bound to the same variable, and clicking to display the inputs will display them all at once.
<template>
<div class="container">
<div v-for="(item, index) in items" :key="index">
<icon #click="showTextbox"/>
<el-input v-if="displayTextbox" v-model="textInput" />
<el-button v-if="displayTextbox" type="primary" #click="confirm" />
<ItemDisplay :data-id="item.id" />
</div>
</div>
</template>
<script>
import ItemDisplay from '#/components/ItemDisplay';
export default {
name: 'ItemList',
components: {
ItemDisplay,
},
props: {
items: {
type: Array,
required: true,
},
}
data() {
displayTextbox = false,
textInput = '',
},
methods: {
confirm() {
// todo send request here
this.displayTextbox = false;
},
showTextbox() {
this.displayTextbox = true;
}
}
}
</script>
EDIT: with the help of #kissu here's the updated and working version
<template>
<div class="container">
<div v-for="(item, index) in itemDataList" :key="itemDataList.id">
<icon #click="showTextbox(item.id)"/>
<El-Input v-if="item.displayTextbox" v-model="item.textInput" />
<El-Button v-if="item.displayTextbox" type="primary" #click="confirm(item.id)" />
<ItemDisplay :data-id="item.item.uuid" />
</div>
</div>
</template>
<script>
import ItemDisplay from '#/components/ItemDisplay';
export default {
name: 'ItemList',
components: {
ItemDisplay,
},
props: {
items: {
type: Array,
required: true,
},
}
data() {
itemDataList = [],
},
methods: {
confirm(id) {
const selected = this.itemDataList.find(
(item) => item.id === id,
)
selected.displayTextbox = false;
console.log(selected.textInput);
// todo send request here
},
showTextbox(id) {
this.itemDataList.find(
(item) => item.id === id,
).displayTextbox = true;
},
populateItemData() {
this.items.forEach((item, index) => {
this.itemDataList.push({
id: item.uuid + index,
displayTextbox: false,
textInput: '',
item: item,
});
});
}
},
created() {
// items prop is obtained from parent component vuex
// generate itemDataList before DOM is rendered so we can render it correctly
this.populateItemData();
},
}
</script>
[assuming you're using Vue2]
If you want to interact with multiple displayTextbox + textInput state, you will need to have an array of objects with a specific key tied to each one of them like in this example.
As of right now, you do have only 1 state for them all, meaning that as you can see: you can toggle it for all or none only.
You'll need to refactor it with an object as in my above example to allow a case-per-case iteration on each state individually.
PS: :key="index" is not a valid solution, you should never use the index of a v-for as explained here.
PS2: please follow the conventions in terms of component naming in your template.
Also, I'm not sure how deep you were planning to go with your components since we don't know the internals of <ItemDisplay :data-id="item.id" />.
But if you also want to manage the labels for each of your inputs, you can do that with nanoid, that way you will be able to have unique UUIDs for each one of your inputs, quite useful.
Use an array to store the values, like this:
<template>
<div v-for="(item, index) in items" :key="index">
<el-input v-model="textInputs[index]" />
</div>
<template>
<script>
export default {
props: {
items: {
type: Array,
required: true,
},
},
data() {
textInputs: []
}
}
</script>

Calling function from outside of component but rendered in v-for loop

I have some component which is rendered 6 times in v-for loop. I'm trying to do onclick function which will call method inside of specified chart component. But now i'm getting errors like this.$refs.chart1.pauseChart() is not an function. This is how i'm trying to achieve it:
<BaseChart ref="`chart$[index]`" #click="pauseChart(index)"/>
pauseChart(index) {
this.$refs.[`chart${index}`].pauseChart()
}
refs inside v-for are arrays rather than singulars. Therefore, if you have
<template v-for="(something, index) in list">
<BaseChart ref="chart" :key="index" #click="pauseChart(index)" />
</template>
you should use
methods:
{
pauseChart(idx)
{
this.$refs.chart[idx].pauseChart();
}
}
For more information - refer to Vue documentation
ref will be having an array. hence, you have to access that with 0 index.
Live Demo :
Vue.component('child', {
methods: {
pauseChart() {
console.log('child method call');
}
},
props: ['childmsg'],
template: '<p v-on="$listeners">{{ childmsg }}</p>',
});
var app = new Vue({
el: '#app',
methods: {
pauseChart(chartIndex) {
this.$refs[chartIndex][0].pauseChart();
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="app">
<div v-for="index in 6" :key="index">
<child childmsg="Hello Vue!" :ref="`chart${index}`" #click="pauseChart(`chart${index}`)">
</child>
</div>
</div>

Vue modal component using in parent component

I'm building simple modal component with Vue. I want to use this component in a parent components and to be able to toggle it from the parent.
This is my code now of the modal component:
<script>
export default {
name: 'Modal',
data() {
return {
modalOpen: true,
}
},
methods: {
modalToggle() {
this.modalOpen = !this.modalOpen
},
},
}
</script>
<template>
<div v-if="modalOpen" class="modal">
<div class="body">
body
</div>
<div class="btn_cancel" #click="modalToggle">
<i class="icon icon-cancel" />
</div>
</div>
</template>
I use the v-if to toggle the rendering and it works with the button i created inside my modal component.
However my problem is: I don't know how to toggle it with simple button from parent component. I don't know how to access the modalOpen data from the modal component
Ok, let's try to do it right. I propose to make a full-fledged component and control the opening and closing of a modal window using the v-model in parent components or in other includes.
1) We need declare prop - "value" in "props" for child component.
<script>
export default {
name: 'Modal',
props: ["value"],
data() {
return {
modalOpen: true,
}
},
methods: {
modalToggle() {
this.modalOpen = !this.modalOpen
},
},
}
</script>
2) Replace your "modalToggle" that:
modalToggle() {
this.$emit('input', !this.value);
}
3) In parent components or other includes declare "modal=false" var and use on component v-model="modal" and any control buttons for modal var.
summary
<template>
<div v-if="value" class="modal">
<div class="body">
body
</div>
<div class="btn_cancel" #click="modalToggle">
<i class="icon icon-cancel" />
</div>
</div>
</template>
<script>
export default {
name: "Modal",
props: ["value"],
methods: {
modalToggle() {
this.$emit("input", !this.value);
}
}
};
</script>
Example:
Vue.component('modal', {
template: '<div v-if="value" class="modal"><div class="body">modal body</div><div class="btn_cancel" #click="modalToggle">close modal<i class="icon icon-cancel" /></div></div>',
props: ["value"],
methods: {
modalToggle() {
this.$emit('input', !this.value);
}
}
});
// create a new Vue instance and mount it to our div element above with the id of app
var vm = new Vue({
el: '#app',
data:() =>({
modal: false
}),
methods: {
openModal() {
this.modal = !this.modal;
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div #click="openModal">Btn modal</div>
<modal v-model="modal"></modal>
</div>
With your current implementation I would suggest you to use refs
https://v2.vuejs.org/v2/guide/components-edge-cases.html#Accessing-Child-Component-Instances-amp-Child-Elements
So in your parent component add ref="child" to modal (child) component and then open your modal by calling this.$refs.child.modalToggle()
You can use an "activator" slot
You can use ref="xxx" on the child and access it from the parent

Cannot access element shown by v-if in component mounted callback

<template>
<div>
<transition name="fade" mode="out-in">
<div class="ui active inline loader" v-if="loading" key="loading"></div>
<div v-else key="loaded">
<span class="foo" ref="foo">the content I'm after is here</span>
</div>
</transition>
</div>
</template>
<script>
export default {
data() {
return {
loaded: false
}
},
mounted() {
setTimeout(() => { // simulate async operation
this.loaded = true
console.log($(this.$refs.foo).length, $(this.$el.find('.foo')).length)
}, 2000)
},
}
</script>
Regardless if I use this.$refs or this.$el, I'm only able to access the loader div (<div class="ui active inline loader"/>).
How am I supposed to access an element which doesn't exist when the component is mounted? Do I have to change v-if to v-show?
Vue renders to the DOM asynchronously. So, even though you are setting your loaded property to true, the ref will not exist until the next tick in Vue's cycle.
To handle that, use the $nextTick method.
console.clear()
new Vue({
el: "#app",
data:{
loading: true
},
mounted(){
setTimeout(()=> {
this.loading = false
this.$nextTick(() => console.log(this.$refs.done))
}, 1000)
}
})
<script src="https://unpkg.com/vue#2.4.2"></script>
<div id="app">
<div v-if="loading">Loading</div>
<div ref="done" v-else>Done</div>
</div>
Additionally, in the question, the v-if expression is loading which will always be undefined because the data property is called loaded.

Vuejs 2.1.10 method passed as prop not a function

I'm very new to Vuejs and JS frameworks in general, so bear with me. I'm trying to call a method that resides in my root component from a child component (2 levels deep) by passing it as a prop, but I get the error:
Uncaught TypeError: this.onChange is not a function
at VueComponent._onChange (category.js:8)
at boundFn (vendor.js?okqp5g:361)
at HTMLInputElement.invoker (vendor.js?okqp5g:2179)
I'm not sure if I'm on the right track by assigning the prop to a method inside the child component, but see what you think:
index.js
var app = new Vue({
el: '#app',
data: function () {
return {
categories: [],
articles: []
}
},
methods: {
onChange: function () {
console.log('first one');
return function () {
console.log('second one');
}
}
},
});
The html:
<div id="app">
<sidebar :onChange=onChange :categories=categories></sidebar>
<varticles :articles=articles></varticles>
</div>
sidebar.js:
Vue.component('sidebar', {
props: ['onChange', 'categories'],
methods: {
_onChange: function () {
this.onChange();
}
},
template: `
<div class="sidebar">
<category v-for="item in categories" :onChange="_onChange" v-bind:category="item"></category>
</div>
`
});
category.js:
Vue.component('category', {
props: ['category', 'onChange'],
methods: {
_onChange: function () {
this.onChange();
}
},
template: `
<div class="category">
<h2>{{ category.name }}</h2>
<ul>
<li v-for="option in category.options">
<input v-on:change="_onChange" v-bind:id="option.tid" type="checkbox" v-model="option.checked">
<label v-bind:for="option.tid">{{ option.name }}</label>
</li>
</ul>
</div>
`
});
There's got to be simpler way to do this!
I'd suggest taking a look at this https://v2.vuejs.org/v2/guide/components.html#camelCase-vs-kebab-case. A simplified version of your code is in this fiddle https://jsfiddle.net/z11fe07p/641/
When writing props in your templates declare them without Capital letters.
A prop declared as onChange in your props is equivalent to on-change in your html.
<sidebar :on-change=onChange :categories=categories></sidebar>
Also I would suggest looking at events and non parent-child communication if you want a link between components that are more than 1 level deep. https://v2.vuejs.org/v2/guide/components.html?#Non-Parent-Child-Communication