So, I have an app with multiple child components. Basically a spreadsheet.
I want to be able to calculate the sum of the components when any cell changes. I've figured out a way to store all the values of the cells by caching them when a change event is propagated. But, is this the best way? Is there a better way to dynamically grab the children? I understand props are the way to send data down, but how do I pull data up?
This is the HTML:
<html>
<head>
</head>
<body>
<span id="calculator">
<template v-for="i in 5">
<cell v-bind:index="i" v-on:total="total"></cell>
</template>
{{ subtotal }}
{{ cells }}
</span>
<script src="vue.js"></script>
<script src="app.js"></script>
</body>
</html>
And the app.js:
Vue.component( 'cell', {
template: "<input v-model='value' v-on:change='total' size='10' type='text'/>",
props: {
index: Number
},
data: function() {
return {
value: 0
};
},
methods: {
total: function() {
console.log( "Value is now: " + this.value + " for index: " + this.index )
this.$emit( 'total', this.value, this.index )
}
}
});
var app = new Vue( {
data: {
subtotal: 0,
cells: []
},
el: "#calculator",
methods: {
total: function( value, indexPlusOne ) {
var index = indexPlusOne-1;
var v = parseInt( value );
Vue.set( this.cells, index, v);
console.log( "Inside the total function: " + v + " " + index );
this.subtotal = 0;
for( var i = 0; i < this.cells.length; i++ ) {
if( this.cells[i] ) {
this.subtotal += this.cells[i];
}
}
}
}
});
I understand props are the way to send data down, but how do I pull data up?
The best way is to use v-model for your custom cell component to pull data up.
Ref: https://v2.vuejs.org/v2/guide/components.html#Form-Input-Components-using-Custom-Events
As explained in the link above, <input v-model="something"> is a syntactic sugar for:
<input v-bind:value="something" v-on:input="something = $event.target.value">
So, your ideal solution would be like:
<cell v-model="item" v-for="item in all_cell_items"></cell>
From within the cell component, you may pass value back to parent (root) component by: this.$emit("input", newValue). The parent component (root) remains clean, and you may simply use a computed property for subTotal.
But this will not work if you have a plain list of integers, like this.cells = [1,2,3,4] and attempt to pass the values to cell component using v-model. You will get the following error:
[Vue warn]: : You are binding v-model directly to a v-for iteration alias. This will not be able to modify the v-for source array because writing to the alias is like modifying a function local variable. Consider using an array of objects and use v-model on an object property instead.
If you are ok to modify your this.cells to array of objects, then you have a clean way of doing it like:
<cell v-model="item.price" :label="item.name" v-for="item in all_items"></cell>
Here is a working jsFiddle for this example: https://jsfiddle.net/mani04/9b7n3qmt/
Related
I have a list in the vuex store with tables[t1,t2], when t1 updates i have to mutate the tables array in order to have the t1 updates.
For each table i use a <Table v-for="(item, index) in tables" :key="index" :data="item"> where it uses a Table.vue. In Table.Vue i have:
props: {
data: {
type: Object,
default: null
}
},
But when t1 changes it also forces an update to t2, which is understandable because the whole "tables" variable in store gets mutated. But I only want the component to update when there is an actual change in props, so that the t2 doesn't get updated unnecessarily. I've tried to look into memoization, which i used previously in React-projects to fix this problem. I didn't really find anything easy to use and searching "memoization vuejs" didn't really give me alot to go on.
What i do in the mutation in vuex store in order to mutate a table:
my_mutation(state, data) {
let newList = [...state.tables];
let index = newList.findIndex(i => i.tableName === data.tableName);
if (index !== -1) {
newList[index] = data.value;
}
Vue.set(state, 'currentViews', newList);
}
as you can see this code only mutate 1 element at the time in the state.tables, but sets the whole tables variable. This causes the update() function to trigger in the components (notice not created() as this only happens initally when i fetch and populate the tables variable) and rerenders the component.
Since the created() is not triggered when the mutation happens, this tells me that the component is still there and doesn't need to be recreated. The goal here is not to trigger updated() when theres no changes to the props and prevent rerendering of that particular component.
You should probably add some code because what you say you see seems strange...
Look at the following example (updated example to use Vuex...just to be sure)
first button mutate the 1st table's data at index 0. Result = only 1st component re-renders
second button completely replaces data for 2nd table. Result = only 2nd component re-renders
3rd button replaces whole tables array by a new array. Result = both component's re-render (as expected)
4th button for curious people ;) ...completely new tables array composed from existing instances. Result = no re-render at all
5th button (added after Q update)
same way of updating as in question
Result - only 2nd table rerenders! Clear contradiction to what you say...
note that if you have currentViews property in the state from the beggining, you don't need to use Vue.set - simple assignment will suffice
Update: Added updated hook with console.log into mytable component to check if logs correlate with time changes in tables headers - confirmed
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
tables: [
['item 1', 'item 2'],
['item 1', 'item 2']
]
},
mutations: {
mutate1st(state) {
Vue.set(state.tables[0], 0, state.tables[0][0]+'+1')
},
mutateTables(state) {
Vue.set(state.tables, 1, ['item 3', 'item 4'])
},
replaceTables(state) {
state.tables = [
['item 1', 'item 2'],
['item 1', 'item 2']
]
},
newArrayCreatedFromPreviousOnes(state) {
state.tables = [...state.tables] // we are creating new array composed from existing instances
},
sameWayAsInQuestion(state) {
let newList = [...state.tables];
newList[1] = ['item 5', 'item 6'];
Vue.set(state, 'tables', newList);
}
}
})
const mytable = Vue.component('mytable', {
props: ['title', 'data'],
template: `
<div>
<div> {{ title }} ({{ now() }}) </div>
<div v-for="(item, index) in data" :key="index">{{ item }}</div>
<hr>
</div>
`,
methods: {
now() {
const date = new Date();
return date.toLocaleTimeString();
}
},
updated() {
console.log(`${this.title} updated at ${this.now()}`)
}
})
const app = new Vue({
store,
components: { mytable },
template: `
<div>
<button #click="mutate1st">Mutate 1st table data</button>
<button #click="mutateTables">Mutate tables array</button>
<button #click="replaceTables">Replace tables array</button>
<button #click="newArrayCreatedFromPreviousOnes">Just for curiosity...</button>
<button #click="sameWayAsInQuestion">Same way as in question</button>
<hr>
<mytable v-for="(table, index) in $store.state.tables" :key="index" :title="'Table ' + index" :data="table" />
</div>
`,
methods: {
...Vuex.mapMutations(['mutate1st', 'mutateTables', 'replaceTables', 'newArrayCreatedFromPreviousOnes', 'sameWayAsInQuestion'])
}
})
app.$mount("#app")
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.12/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/3.5.1/vuex.min.js"></script>
<div id="app"></div>
Update: I have another component inside Table.vue which was dependent on a store variable which was mutated when changing t1 causing the rest of the tables to change. Tip of the day: check if all the nested component isnt dependent on some store variable effected by the change
How to display the value which is inside computed in VueJS2 ? Actually i'm trying to return the value click*2 using computed property. So i'm displaying the output using the expression {{counter}}. But i'm not getting any output, at first it was set to counter to 0 (stored in the data). Even i didn't use v-once. So why my counter value is not updating through computed property?
<div id="app4">
<p>counter : {{ counter }}</p>
<p>clicks : {{ click }}</p>
<button v-on:click="increment">increment</button>
</div>
<script>
var vm = new Vue({
el: '#app4',
data: {
counter:0,
click: 0,
},
methods: {
increment() {
this.click++;
}
},
computed: {
counter() {
return this.click * 2;
}
}
});
</script>
Remove counter from data and it will work. You have two properties that shadow one another - one in data and the other in computed.
Is there a way to tell Vue to call a method only once when used as an expression?
Here's my code:
<div v-for="i in a.b.c.items">
<div :id="foo(i.value)"></div>
</div>
The way it is now, the foo() method will be executed any time anything on the model changes, not only items. Is there something in Vue that I can tell to evaluate this only once?
like this: <div :id.only-once="foo(i.value)"
Unfortunately that's only possible for certain events, e.g. in this question here. What you may want to consider instead is a computed property where you compute all of these values and return the array. This resulting array will be cached by Vue and will not be reevaluated until your items array is modified (and modified in such a way that Vue will detect the change).
An example:
Vue Setup
<script>
new Vue({
el: '. . .',
data: {
a: {b: {c: {items: [. . .]}}}
},
methods: {
foo: function(val) {
. . .
}
},
computed: {
itemsAfterFoo: function() {
var this_vue_instance = this;
var computed_items = [];
this_vue_instance.items.forEach(function(item) {
computed_items.push(this_vue_instance.foo(item.value));
});
return computed_items;
}
}
});
</script>
Template
<div v-for="(i, index) in a.b.c.items">
<div :id="itemsAfterFoo[index]"></div>
</div>
Or something to that effect.
More information on computed properties here: https://v2.vuejs.org/v2/guide/computed.html
I have a computed property that simply formats the date:
computed: {
items() {
return this.licenseItems.map(function(license) {
license.expires_at = moment(license.expires_at).format('MM/DD/YYYY');
return license;
});
}
}
I pass licenseItems to a form component with the .sync modifier and emit update:field event from the form. In vue dev tools, I can see that licenseItems (data) is properly updated, but items (computed) is still showing the old data so no re-computation was performed.
I have noticed that if I remove the map and just return the licenseItems object from the computed property, it is updated. Is there some issue with Vue's computed property on mapped objects when using the sync modifier?
You should be aware that you're modifying underlying objects in your computed. From your fiddle:
computed: {
mapped: function() {
return this.items.map(function(item) {
let original = item.date;
item.date = moment(item.date).format('MM/DD/YYYY');
console.log(original + ' => ' + item.date);
return item;
});
}
}
Your incrementDates function also modifies the underlying objects directly.
Because each element of items is an object, item is a reference to that same object, so your routine updates the members of items itself. If you intend to modify the object values, you should use a watch instead of a computed. If you want to have a proper computed that does not modify data items, you need to deep copy the objects.
In my example below, you can see that the data value and the computed value are distinct, but the computed is based on the data. Also, the update event works with sync to update the value in the parent, rather than the value being updated directly in the component. You can enter any format of date that Date understands to set the value.
new Vue({
el: '#app',
data: {
licenseItems: [{
expires_at: Date.now()
}]
},
computed: {
items() {
return this.licenseItems.map(function(license) {
const newItem = Vue.util.extend({}, license);
newItem.expires_at = moment(license.expires_at).format('MM/DD/YYYY');
return newItem;
});
}
},
components: {
myUpdater: {
props: ['items'],
methods: {
doUpdate(event, index) {
const newObj = this.items.map(item => Vue.util.extend({}, item));
newObj[index].expires_at = new Date(event.target.value);
console.log("new object", newObj);
this.$emit('update:items', newObj);
}
}
}
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.19.1/moment.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<div id="app">
<my-updater :items.sync="licenseItems" inline-template>
<div>
<input v-for="item, index in items" :value="item.expires_at" #change="doUpdate($event, index)">
</div>
</my-updater>
<div v-for="item in items">
{{item.expires_at}}
</div>
</div>
How to reference text that's in in Vue.js?
Vue.component('component', {
template: `<button><slot></slot></button>`,
created: function() {
// i would like to access the text in slot here
}
});
Note: This answer applies to Vue v2 only.
The content inside the default slot, which is what you are describing, is exposed as this.$slots.default in the Vue. So the most naive way to get the text inside your button would be to use this.$slots.default[0].text.
Vue.component('component', {
template: `<button><slot></slot></button>`,
created: function() {
const buttonText = this.$slots.default[0].text;
}
});
The problem is that there may be more than one node inside the slot, and the nodes may not necessarily be text. Consider this button:
<button><i class="fa fa-check"></i> OK</button>
In this case, using the first solution will result in undefined because the first node in the slot is not a text node.
To fix that we can borrow a function from the Vue documentation for render functions.
var getChildrenTextContent = function (children) {
return children.map(function (node) {
return node.children
? getChildrenTextContent(node.children)
: node.text
}).join('')
}
And write
Vue.component("mybutton", {
template:"<button><slot></slot></button>",
created(){
const text = getChildrenTextContent(this.$slots.default);
console.log(text)
}
})
Which will return all the text in the slot joined together. Assuming the above example with the icon, it would return, "OK".
For Vue 3.
The answer from #bert works well on Vue 2, but Vue 3 slots have a more complex structure.
Here is one way to get the slots text contents (from default slot) on Vue 3.
const getSlotChildrenText = children => children.map(node => {
if (!node.children || typeof node.children === 'string') return node.children || ''
else if (Array.isArray(node.children)) return getSlotChildrenText(node.children)
else if (node.children.default) return getSlotChildrenText(node.children.default())
}).join('')
const slotTexts = this.$slots.default && getSlotChildrenText(this.$slots.default()) || ''
console.log(slotTexts)
Run the code snippet below that get the slot text passed by parent :
I'm using "ref" :
<span ref="mySlot">
this.$refs.mySlot.innerHTML
Careful : <slot ref="refName"></slot> don't works because <slot> are not render on html.
You have to wrap the <slot></slot> with <div></div> or <span></span>
The code :
Vue.component('component', {
template: '<button>' +
'<span ref="mySlot">' +
'Text before<br />' +
'<slot name="slot1">' +
'Text by default' +
'</slot>' +
'<br />Text after' +
'</span>' +
'</button>',
mounted: function() {
console.log( this.$refs.mySlot.innerHTML);
}
});
new Vue({
el: '#app'
});
<script src="https://vuejs.org/js/vue.min.js"></script>
<div id="app">
<component>
<span slot="slot1">I'm overriding the slot and text appear in this.$refs.mySlot.innerHTML !</span>
</component>
</div>
You can access the slot text by joining the innerText of all the children inside the slot.
getSlotText() {
return this.$slots.default.map(vnode => (vnode.text || vnode.elm.innerText)).join('');
},
My use case was pretty simple, I had a default slot with only text.
Here's how I accessed the text in vue3 with script setup:
<script setup lang="ts">
import { computed, useSlots } from "vue";
const slots = useSlots();
const slotText = computed(() => {
return slots.default()[0].children; // This is the interesting line
});
</script>