Vue data, computed, and methods - vue.js

Wondering if I should directly update a potentially large array in data() using a method, or instead, have the method set another primitive property in data() and use computed to return an altered array using that? Examples of both approaches below:
Approach #1:
data() {
return {
users: [
{
name: 'alice'
selected: true
},
name: 'bob'
selected: false
// ...
methods: {
handleSelection(selectedIndex) {
this.users = this.users.map((item, index) => {
item.selected = selectedIndex === index ? true : false;
return item;
});
}
Approach #2:
data() {
return {
selectedIndex: 0,
users: [
{
name: 'alice'
selected: true
},
name: 'bob'
selected: false
// ...
computed: {
usersSelected() {
return this.users.map((item, index) => {
item.selected = this.selectedIndex === index ? true : false;
return item;
});
}
//...
methods: {
handleSelection(selectedIndex) {
this.selectedIndex = selectedIndex;
}
Is one better than the other or any additional suggestions/improvements? Thanks!

You should use a computed property. That way, you don't need to care about remembering to call various methods, to keep data in sync.
Your computed property can be made a bit more simple, with your requirements.
new Vue({
el: '#app',
data: {
selectedUserIndex: null,
users: [{
name: 'Mark',
id: 1
},
{
name: 'John',
id: 2
},
{
name: 'Evan',
id: 3
}
]
},
computed: {
selectedUser() {
return this.selectedUserIndex >= 0
? this.users[this.selectedUserIndex]
: null
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<p>
Selected user:
<span v-if="selectedUser">{{selectedUser.name}}</span>
<em v-else>No user selected</em>
</p>
<ul>
<li #click="selectedUserIndex = index" v-for="(user, index) in users" :key="user.id">
{{user.name}}
</li>
</ul>
</div>

Related

How to pass argument to $store.getters with onclick event?

How to pass argument to $store.getters with onclick event? I can see the default value but not the new value. This is my code
const store = new Vuex.Store({
state: {
notes: [
{ id: 1, text: 'Hello World 1, Vuex!', deleted: false},
{ id: 2, text: 'Hello World 2, Vuex!', deleted: true},
{ id: 3, text: 'Hello World 3, Vuex!', deleted: false}
]
},
getters: {
getNoteById: (state) => (id) => {
return state.notes.find(note => note.id === id);
}
}
});
new Vue({
el: "#app",
data() {
return {
result: ''
}
},
store,
computed: {
note() {
return this.$store.getters.getNoteById(3);
}
},
methods: {
showText(id) {
this.result = this.$store.getters.getNoteById(id);
}
}
});
and html
<div id='app'>
<p>Default value: {{ note.text }}</p>
<button #click="showText('1')">1</button>
<button #click="showText('2')">2</button>
<button #click="showText('3')">3</button>
<p>New value: {{ result.text }}</p>
</div>
Your getter is working perfectly. And it correctly returns undefined when searching for any item with an id of '1', '2' or '3'.
Because the store doesn't contain any such item.
All your store items have number ids, none of them has a string id. See it working (I changed the template to send numbers):
Vue.config.productionTip = false;
Vue.config.devtools = false;
const store = new Vuex.Store({
state: {
notes: [
{ id: 1, text: 'Hello World 1, Vuex!', deleted: false},
{ id: 2, text: 'Hello World 2, Vuex!', deleted: true},
{ id: 3, text: 'Hello World 3, Vuex!', deleted: false}
]
},
getters: {
getNoteById: (state) => (id) => {
return state.notes.find(note => note.id === id);
}
}
});
new Vue({
el: "#app",
data() {
return {
result: ''
}
},
store,
computed: {
note() {
return this.$store.getters.getNoteById(3);
}
},
methods: {
showText(id) {
this.result = this.$store.getters.getNoteById(id);
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.14/vue.js"></script>
<script src="https://unpkg.com/vuex"></script>
<div id='app'>
<p>Default value: {{ note.text }}</p>
<button #click="showText(1)">1</button>
<button #click="showText(2)">2</button>
<button #click="showText(3)">3</button>
<p>New value: {{ result.text }}</p>
</div>
If, for any reason, you don't have control over the incoming type (you might have this problem when reading the value of an <input>, which is always a string, even when the type of the input is "number"), you might want to cast the value as number: +'5' => 5.
So, as an alternative to fixing the template, as above, you could do this in the method:
showText(id) {
this.result = this.$store.getters.getNoteById(+id);
}

Handle interaction between vue fields

I have prepared a functional code example in JSFiddle of VUE field interaction.
https://jsfiddle.net/JLLMNCHR/2a9ex5zu/6/
I have a custom autocomplete component that works properly, a normal input field, and a 'Load' button which objetive is to load the value entered in the normal input in the autocomplete field.
This 'load' button is not working.
HTML:
<div id="app">
<p>Selected: {{test1}}</p>
<br>
<div>
<label>Test1:</label>
<keep-alive>
<autocomplete v-model="test1" v-bind:key="1" :items="theItems">
</autocomplete>
</keep-alive>
</div>
<br>
<label>Display this in 'test1':</label>
<input type="text" v-model=anotherField>
<button type="button" v-on:click="loadField()">Load</button>
<br>
<br>
<button type="button" v-on:click="displayVals()">Display vals</button>
</div>
<script type="text/x-template" id="autocomplete">
<div class="autocomplete">
<input type="text" #input="onChange" v-model="search"
#keyup.down="onArrowDown" #keyup.up="onArrowUp" #keyup.enter="onEnter" />
<ul id="autocomplete-results" v-show="isOpen" class="autocomplete-results">
<li class="loading" v-if="isLoading">
Loading results...
</li>
<li v-else v-for="(result, i) in results" :key="i" #click="setResult(result)"
class="autocomplete-result" :class="{'is-active':i === arrowCounter}">
{{ result }}
</li>
</ul>
</div>
</script>
VUE.JS:
const Autocomplete = {
name: "autocomplete",
template: "#autocomplete",
props: {
items: {
type: Array,
required: false,
default: () => []
},
isAsync: {
type: Boolean,
required: false,
default: false
}
},
data() {
return {
isOpen: false,
results: [],
search: "",
isLoading: false,
arrowCounter: 0
};
},
methods: {
onChange() {
// Let's warn the parent that a change was made
this.$emit("input", this.search);
// Is the data given by an outside ajax request?
if (this.isAsync) {
this.isLoading = true;
} else {
// Let's search our flat array
this.filterResults();
this.isOpen = true;
}
},
filterResults() {
// first uncapitalize all the things
this.results = this.items.filter(item => {
return item.toLowerCase().indexOf(this.search.toLowerCase()) > -1;
});
},
setResult(result) {
this.search = result;
this.$emit("input", this.search);
this.isOpen = false;
},
onArrowDown(evt) {
if (this.arrowCounter < this.results.length) {
this.arrowCounter = this.arrowCounter + 1;
}
},
onArrowUp() {
if (this.arrowCounter > 0) {
this.arrowCounter = this.arrowCounter - 1;
}
},
onEnter() {
this.search = this.results[this.arrowCounter];
this.isOpen = false;
this.arrowCounter = -1;
},
handleClickOutside(evt) {
if (!this.$el.contains(evt.target)) {
this.isOpen = false;
this.arrowCounter = -1;
}
}
},
watch: {
items: function(val, oldValue) {
// actually compare them
if (val.length !== oldValue.length) {
this.results = val;
this.isLoading = false;
}
}
},
mounted() {
document.addEventListener("click", this.handleClickOutside);
},
destroyed() {
document.removeEventListener("click", this.handleClickOutside);
}
};
new Vue({
el: "#app",
name: "app",
components: {
autocomplete: Autocomplete
},
methods: {
displayVals() {
alert("test1=" + this.test1);
},
loadField() {
this.test1=this.anotherField;
}
},
data: {
test1: '',
anotherField: '',
theItems: [ 'Apple', 'Banana', 'Orange', 'Mango', 'Pear', 'Peach', 'Grape', 'Tangerine', 'Pineapple']
}
});
Any help will be appreciated.
See this new fiddle where it is fixed.
When you use v-model on a custom component you need to add a property named value and watch it for changes, so it can update the local property this.search.

vuejs reactivity of complex objects

I am using Vue.js 2.5.17 I have two components, App (parent) and TreeNode (child), which display a tree structure of items from a deeply nested object. Each node in the tree is presented with the TreeNode component which is a recursive component.
TreeNode component
const TreeNode = Vue.component('TreeNode', {
name: 'TreeNode',
template: '#treenode',
props: {
model: Object,
},
data() {
return {
open: false,
};
},
computed: {
isExpandable() {
return this.model.children &&
this.model.children.length;
},
},
methods: {
toggle() {
if (this.isExpandable) {
this.open = !this.open;
}
},
},
});
TreeNode template
<script type="text/x-template" id="treenode">
<li>
<input type="checkbox" :id="model.name" style="display:none;"/>
<label :for="model.name" style="color:gray;" #click="toggle">
{{ model.name }}
{{ model.data.example }}
</label>
<ul v-show="open" v-if="isExpandable">
<TreeNode
class="item"
v-for="(model, index) in model.children"
:key="index"
:model="model">
</TreeNode>
</ul>
</li>
</script>
App component template
<script type="text/x-template" id="oulist">
<div>
<div id="unitsTable" class="row filterlist treelist b_widget2" style="width:85%;">
<div class="css-treeview">
<TreeNode
class="item"
:model="items">
</TreeNode>
</div>
</div>
</script>
App component
const App = Vue.component('App', {
template: '#oulist',
components: {
TreeNode,
},
data() {
return {
items: {
name: 'item1',
data: { example: '1' },
children: [
{
name: 'item11',
children: [],
data: { example: '1' },
},
{
name: 'item12',
children: [
{ name: 'item121', children: [], data: { example: '1' } },
{ name: 'item122', children: [], data: { example: '1' } },
],
data: { example: '1' },
},
],
},
};
},
methods: {
updateItem(currNode, name, data) {
if (currNode.name === name) {
Object.assign(currNode.data, data);
this.items = Object.assign({}, this.items); // tried to create a new object here and overwrite it, but it didn't help
return;
}
if (currNode.children) {
currNode.children.forEach(c => this.updateItem(c, name, data));
}
},
},
});
The object posted above is just an example, my actual object has a lot more nested levels and items per level.
The problem am I facing is that whenever a property deep within my items object is changed (more specifically, the example property of the data object inside a node), the DOM is not updated. I read through the reactivity caveats and saw that adding new properties is not reactive by default, but I am not adding new properties here, just changing the existing ones.
When data from a tree node is updated, I traverse the object to find the correct node and update its properties as follows:
updateItem(currNode, name, data) {
if (currNode.name === name) {
Object.assign(currNode.data, data);
this.items = Object.assign({}, this.items); // tried to create a new object here and overwrite it, but it didn't help
return;
}
if (currNode.children) {
currNode.children.forEach(c => this.updateItem(c, name, data));
}
},
this.updateItem(this.items, 'item121', { example: 'newVal' });
Any tips ? Thanks!
EDIT: The data is always changed only in the parent (App) component.

Why is the computed property of this component not reactive:

I have an object like this:
myObject: {
items: [{
title: '140',
isActive: true,
}, {
title: '7',
isActive: false
}, {
title: '10',
isActive: false
}]
}
Which I'm using like this:
<my-component :items="myObject.items"></my-component>
This is how the component looks like:
<template>
<div class="i-panel panel-container d-flex"
<div
v-for="item in prefs.items"
class="panel-item"
#click="onClick(item)">
<!-- some content -->
</div>
</div>
</template>
<script>
export default {
name: 'IPanel',
props: {
items: {
type: Array,
default () {
return []
}
}
},
computed: {
// code
prefs () {
return {
items: this.items
}
}
},
methods: {
onClick (item) {
this.prefs.items.forEach(item => {
if (JSON.stringify(item) === JSON.stringify(clickedItem)) {
item.isActive = true
}
})
}
}
}
</script>
When I click an item (and that item is the same as the clickedItem), it's supposed to become isActive true. It does. But I have to refresh the Vue devtools or re-render the page for the change to take effect.
Why isn't item.isActive = true reactive?
In the code you posted, you are using a clickedItem object that is not defined anywhere. I don't know if this is just in the process of writing your question or if it is your problem.
However, when using clickedItem the right way, it seems to work: https://jsfiddle.net/d5z93ygy/4/
HTML
<div id="app" class="i-panel panel-container d-flex">
<div
v-for="item in prefs.items"
class="panel-item"
#click="onClick(item)">
<!-- some content -->{{ item.isActive ? 'active' : 'inactive' }}
</div>
</div>
JS
new Vue({
el: "#app",
data: {
items: [{
title: '140',
isActive: true,
}, {
title: '7',
isActive: false
}, {
title: '10',
isActive: false
}]
},
computed: {
// code
prefs () {
return {
items: this.items
}
}
},
methods: {
onClick (clickedItem) {
this.prefs.items.forEach(item => {
if (JSON.stringify(item) === JSON.stringify(clickedItem)) {
item.isActive = true
}
})
}
}
})
Change
<div
v-for="item in prefs.items"
class="panel-item"
#click="onClick(item)">
<!-- some content -->
</div>
to
<div
v-for="(item, index) in prefs.items"
class="panel-item"
#click="onClick(index)">
<!-- some content -->
</div>
Then, in your change method, go like this:
onClick (index) {
Vue.set(this.items, index, true);
}
https://v2.vuejs.org/v2/guide/list.html#Object-Change-Detection-Caveats

VueJs: Form handling with Vuex and inputs generated with an API

Here's an example of a component:
<script>
export default {
name: 'my-form',
computed: {
myModules() {
return this.$store.state.myModules;
}
}
</script>
<template>
<form>
<p v-for="module in myModules">
<input type="checkbox" :value="module.id" />
<label>module.name</label>
</p>
<button type="submit">Submit</button>
</form>
</template>
The associated store:
state: {
myModules: []
},
mutations: {
setModules(state, modules) {
state.myModules = modules;
}
},
actions: {
getModules({commit}) {
return axios.get('modules')
.then((response) => {
commit('setModules', response.data.modules);
});
}
}
And finally, an example of return of the API "getModules":
modules : [
{
id: 1,
name: 'Module 1',
isActive: false
},
{
id: 2,
name: 'Module 2',
isActive: false
},
{
id: 3,
name: 'Module 3',
isActive: false
}
]
My question: what's the best way to change the "isActive" property of each module to "true" when I check the checkbox corresponding to the associated module, directly in the store?
I know that Vuex's documentation recommends to use "Two-way Computed Property" to manage the forms, but here I don't know the number of modules that the API can potentially return, and I don't know their name.
Thank you in advance!
This is a little bit wicked approach, but it works. You can create an accessor object for every item you access in a loop:
const store = new Vuex.Store({
mutations: {
setActive (state, {index, value}) {
state.modules[index].isActive = value
}
},
state: {
modules : [
{
id: 1,
name: 'Module 1',
isActive: false
},
{
id: 2,
name: 'Module 2',
isActive: false
},
{
id: 3,
name: 'Module 3',
isActive: false
}
]
}
});
const app = new Vue({
el: '#target',
store,
methods: {
model (id) {
const store = this.$store;
// here i return an object with value property that is bound to
// specific module and - thanks to Vue - retains reactivity
return Object.defineProperty({}, 'value', {
get () {
return store.state.modules[id].isActive
},
set (value) {
store.commit('setActive', {index: id, value});
}
});
}
}
})
<script src="https://unpkg.com/vue/dist/vue.min.js"></script>
<script src="https://unpkg.com/vuex/dist/vuex.min.js"></script>
<div id="target">
<div v-for="(item, id) in $store.state.modules">
Module #{{ item.id }} state: {{ item.isActive }}
</div>
<div v-for="(item, id) in $store.state.modules">
<label>
Module #{{ item.id }}
<input type="checkbox" v-model="model(id).value"/>
</label>
</div>
</div>
This is still quite a messy approach, but at least you don't have to commit mutations directly in template. With a little help of Vue.set() you can use this approach even to overcome standard reactivity caveats.
I have an alternative solution for you. You could make a child component for the checkboxes to clean up the code a bit.
UPD: I just realised that everything that I and #etki proposed is an overkill. I left the old version of my code below in case you still want to take a look. Here is a new one:
const modules = [{
id: 1,
name: 'Module 1',
isActive: true,
},
{
id: 2,
name: 'Module 2',
isActive: false,
},
{
id: 3,
name: 'Module 3',
isActive: false,
},
];
const store = new Vuex.Store({
state: {
myModules: [],
},
mutations: {
SET_MODULES(state, modules) {
state.myModules = modules;
},
TOGGLE_MODULE(state, id) {
state.myModules.some((el) => {
if (el.id === id) {
el.isActive = !el.isActive;
return true;
}
})
}
},
actions: {
getModules({
commit
}) {
return new Promise((fulfill) => {
setTimeout(() => {
commit('SET_MODULES', modules);
fulfill(modules);
}, 500)
});
}
}
});
const app = new Vue({
el: "#app",
store,
data: {},
methods: {
toggle(id) {
console.log(id);
this.$store.commit('TOGGLE_MODULE', id);
}
},
computed: {
myModules() {
return this.$store.state.myModules;
},
output() {
return JSON.stringify(this.myModules, null, 2);
},
},
mounted() {
this.$store.dispatch('getModules').then(() => console.log(this.myModules));
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/3.0.1/vuex.js"></script>
<script src="https://unpkg.com/vue"></script>
<div id="app">
<form>
<div v-for="data in myModules">
<label :for="data.id">{{ data.name }}: {{data.isActive}}</label>
<input type="checkbox" :id="data.id" :name="'checkbox-' + data.id" :checked="data.isActive" #change="toggle(data.id)">
</div>
</form>
<h3>Vuex state:</h3>
<pre v-text="output"></pre>
</div>
As you can see above you could just call a function on input change and pass an id as a parameter to a method that fires vuex action.
The old version of my code.
A new one on jsfiddle