Vue: Why aren't list's items responsive? - vue.js

I have a list of items that I pass to the component as props. Then I want to create a computed list mapped on the props list. What I do:
computed: {
assets: function(){
var self = this;
var mappedList = [];
if (self.sourceList && self.sourceList.length){
mappedList = _.map(self.sourceList, function(asset){
return { title: asset.title, id: asset.id};
});
}
return mappedList;
}
}
I can see the mapped list then rendered:
<div v-for="asset in assets" :key="asset.id">
<span>{{asset.title}}</span>
<input v-model="source.title" />
</div>
I can't figure out why but when I change a title it's not changed for the item is being edited.
(If I try just create a new list and use push method to add items into it, the result is the same).
UPD Solution found. As #WW noted some data should be used. I just move the list to the data and watch props and it worked.

Related

Vue class components dynamically add component depending on answer from backend

So from the backend I get a array of objects that look kind of like this
ItemsToAdd
{
Page: MemberPage
Feature: Search
Text: "Something to explain said feature"
}
So i match these values to enums in the frontend and then on for example the memberpage i do this check
private get itemsForPageFeatures(): ItemsToAdd[] {
return this.items.filter(
(f) =>
f.page== Pages.MemberPage &&
f.feature != null
);
}
What we get from the backend will change a lot over time and is only the same for weeks at most. So I would like to avoid to have to add the components in the template as it will become dead code fast and will become a huge thing to have to just go around and delete dead code. So preferably i would like to add it using a function and then for example for the search feature i would have a ref on the parent like
<SearchBox :ref="Features.Search" />
and in code just add elements where the ItemsToAdd objects Feature property match the ref
is this possible in Vue? things like appendChild and so on doesn't work in Vue but that is the closest thing i can think of to kind of what I want. This function would basically just loop through the itemsForPageFeatures and add the features belonging to the page it is run on.
For another example how the template looks
<template>
<div class="container-fluid mt-3">
<div
class="d-flex flex-row justify-content-between flex-wrap align-items-center"
>
<div class="d-align-self-end">
<SearchBox :ref="Features.Search" />
</div>
</div>
<MessagesFilter
:ref="Features.MessagesFilter"
/>
<DataChart
:ref="Features.DataChart"
/>
So say we got an answer from backend where it contains an object that has a feature property DataChart and another one with Search so now i would want components to be added under the DataChart component and the SearchBox component but not the messagesFilter one as we didnt get that from the backend. But then next week we change in backend so we no longer want to display the Search feature component under searchbox. so we only get the object with DataChart so then it should only render the DataChart one. So the solution would have to work without having to make changes to the frontend everytime we change what we want to display as the backend will only be database configs that dont require releases.
Closest i can come up with is this function that does not work for Vue as appendChild doesnt work there but to help with kind of what i imagine. So the component to be generated is known and will always be the same type of component. It is where it is to be placed that is the dynamic part.
private showTextBoxes() {
this.itemsForPageFeatures.forEach((element) => {
let el = this.$createElement(NewMinorFeatureTextBox, {
props: {
item: element,
},
});
var ref = `${element.feature}`
this.$refs.ref.appendChild(el);
});
}
You can use dynamic components for it. use it like this:
<component v-for="item in itemsForPageFeatures" :is="getComponent(item.Feature)" :key="item.Feature"/>
also inside your script:
export default {
data() {
return {
items: [
{
Page: "MemberPage",
Feature: "Search",
Text: "Something to explain said feature"
}
]
};
},
computed: {
itemsForPageFeatures() {
return this.items.filter(
f =>
f.Page === "MemberPage" &&
f.Feature != null
);
}
},
methods: {
getComponent(feature) {
switch (feature) {
case "Search":
return "search-box";
default:
return "";
}
}
}
};

List of components not updating

I have this template displaying a list of components:
<template>
<v-container id="container">
<RaceItem v-for="(item, index) in items" :key="item + index" />
(...)
When the array "items" is updated (shift() or push() a new item), the list displayed is not updated.
I'm sure I'm missing something on how Vue works... but could anyone explain what's wrong?
The key attribute expects number | string | boolean | symbol. It is recommended to use primitives as keys.
The simplest usage is to use a primitive unique property of each element to track them:
<RaceItem v-for="item in items" :key="item.id" />
... assuming your items have unique ids.
If you use the index as key, every time you update the array you'll have to replace it with an updated version of itself (i.e: this.items = [...items]; - where items contains the mutation).
Here's how your methods would have to look in that case:
methods: {
applyChangeToItem(item, change) {
// apply change to item and return the modified item
},
updateItem(index, change) {
this.$set(
this.items,
index,
this.applyChangeToItem(this.items[index], change)
);
},
addItem(item) {
this.items = [...this.items, item];
},
removeItem(index) {
this.items = [...this.items.filter((_, i) => index !== i)];
}
}
Can you try something like this
Put componentKey on v-container and use forceRender() after your push is done

Vuejs2 - How to update the whole object from the server not losing reactivity?

I have a list of objects that can be updated from the database.
So, when I load the list, objects have only id and name.
When I click on an object I load other fields that can be of any length - that's why I don't load them with the objects in the list.
I found that when I update an object it can be difficult to keep reactivity https://v2.vuejs.org/v2/guide/reactivity.html so I need to find some workaround.
this code works almost okay:
axios.get('/api/news', item.id).then(function(response){
if (response){
Object.assign(item, response.data.item);
}
});
But the problem is the fields that have not been presented from the beginning is not 100% reactive anymore. What is going on is a new field has been updated reactively only when I change another, previous one. So, if I show 2 text field with old and new properties, if I change the second property, the field will not be updated until I change the first one.
I got item object from the component:
data () {
return {
items: [],
}
},
and
<div v-for="item in items" #click="selectItem(item)" >
<span>{{item.name}}</span>
</div>
Then item's been passed to the function selectItem.
What is the proper pattern to load new fields and keep them reactive? (NB: it's not the case when I can assign field by field - I want to reuse the same code no matter which object it is, so I need so the solution for updating an object at a time without listing all new fields.)
Note. This code works inside the component.
Completely revised post: Ok, the example you give uses an array, which has its own caveats, in particular that you can't directly set values like vm.items[indexOfItem] = newValue and have it react.
So you have to use Vue.set with the array as the first argument and the index as the second. Here's an example that adds a name property to object items and then uses Vue.set to set the item to a new object created by Object.assign.
new Vue({
el: '#app',
data: {
items: [{
id: 1,
other: 'data'
}, {
id: 2,
other: 'thingy'
}]
},
methods: {
selectItem(parent, key) {
const newObj = Object.assign({}, parent[key], {
name: 'some new name'
});
Vue.set(parent, key, newObj);
setTimeout(() => {parent[key].name = 'Look, reactive!';}, 1500);
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="item, index in items" #click="selectItem(items, index)">
<span>{{item.name || 'empty'}}</span>
</div>
<pre>
{{JSON.stringify(items, null, 2)}}
</pre>
</div>
Have a look at Change-Detection-Caveats Vue cannot detect property addition or deletion if you use "normal assign" methods.
You must use Vue.set(object, key, value)
Try something like the following:
axios.get('/api/news', item.id).then(function(response){
if (response){
let item = {}
Vue.set(item, 'data', response.data.item)
}
});
Than item.data would than be reactiv.
Can simply use Vue.set to update this.item reactively
axios.get('/api/news', item.id).then(function(response){
if (response){
this.$set(this, "item", response.data.item);
}
});

Set a variable inside a v-for loop on Vue JS

I have a v-for loop with vue.js on a SPA and I wonder if it's posible to set a variable at the beginning and then just print it everytime you need it, because right now i'm calling a method everytime i need to print the variable.
This is the JSON data.
{
"likes": ["famiglia", "ridere", "caffè", "cioccolato", "tres leches", "ballare", "cinema"],
"dislikes":["tristezze", "abuso su animali", "ingiustizie", "bugie"]
}
Then I use it in a loop:
<template>
<div class="c-interests__item" v-for="(value, key) in interests" :key="key" :data-key="key" :data-is="getEmotion(key)" >
// NOTE: I need to use the variable like this in different places, and I find myself calling getEmotion(key) everythime, is this the way to go on Vue? or there is another way to set a var and just call it where we need it?
<div :class="['c-card__frontTopBox', 'c-card__frontTopBox--' + getEmotion(key)]" ...
<svgicon :icon="getEmotion(key) ...
</div>
</template>
<script>
import interests from '../assets/json/interests.json'
... More imports
let emotion = ''
export default {
name: 'CInfographicsInterests',
components: {
JSubtitle, svgicon
},
data () {
return {
interests,
emotion
}
},
methods: {
getEmotion (key) {
let emotion = (key === 0) ? 'happy' : 'sad'
return emotion
}
}
}
</script>
// Not relevanty to the question
<style lang='scss'>
.c-interests{...}
</style>
I tried adding a prop like :testy="getEmotion(key)" and then { testy } with no luck...
I tried printing { emotion } directly and it doesn't work
So, there is anyway to acomplish this or should i stick calling the method every time?
Thanks in advance for any help.
It's not a good idea to use methods inside a template for non-user-directed actions (like onClicks). It's especially bad, when it comes to performance, inside loops.
Instead of using a method, you can use a computed variable to store the state like so
computed: {
emotions() {
return this.interests.map((index, key) => key === 0 ? 'happy' : 'sad');
}
}
This will create an array that will return the data you need, so you can use
<div class="c-interests__item"
v-for="(value, key) in interests"
:key="key" />`
which will reduce the amount of times the item gets re-drawn.

Sharing data from a VueJS component

I have a VueJS address lookup component.
Vue.component('address-lookup',
{
template: '#address-lookup-template',
data: function()
{
return {
address: {'name': '', 'town:': '', 'postcode': ''},
errors: {'name': false, 'town': false, 'postcode': false},
states: {'busy': false, 'found': false},
result: {}
}
},
methods:
{
findAddress: function(event)
{
if( typeof event === 'object' && typeof event.target === 'object' )
{
event.target.blur();
}
$.ajax(
{
context: this,
url: '/lookup',
data:
{
'name': this.address.name,
'town': this.address.town,
'postcode': this.address.postcode
},
success: function(data)
{
this.states.busy = false;
this.states.found = true;
this.address.name = data.name;
this.result = data;
}
});
},
reset: function()
{
this.states.found = false;
this.result = {};
}
}
});
Inside my template I've then bound the result like so:
<p>{{ result.formatted_address }}</p>
There is some extra data returned within the result (like a twitter handle) that isn't part of the address lookup template, and occurs on a separate part of the form. For reasons relating to how my form is structured I can't include these inputs within the same template.
I found a way to bind those inputs, although it felt somewhat 'hacky'.
<input type="text" name="twitter" v-model="$refs.lookupResult._data.result.twitter">
That all works fine.
My problem is that the form is included as part of a larger template sometimes in the context of creating a new record, sometimes in the context of editing. When editing a record, the lookup component is removed (using an if server-side, so the template is no longer loaded at all) and when that happens I get this error.
$refs.lookupResult._data.result.twitter": TypeError: Cannot read property '_data' of undefined
This makes sense. lookupResult is defined when I include the template, and when editing I am removing this line:
<address-lookup v-ref:lookup-result></address-lookup>
I've worked around it by including a version of each extra input without the v-model attribute, again using a server-side if. But there are quite a few of these and it's getting a bit messy.
Is there a cleaner approach I could be using to better achieve this?
So I don't know the hierarchy of your layout, it isn't indicated above, but assuming that address-lookup component is a child of your parent, and you in fact need the results of address lookup in that parent, eg:
<parent-component> <!-- where you need the data -->
<address-lookup></address-lookup> <!-- where you lookup the data -->
</parent-component>
then you can simply pass the data props, either top-down only (default) or bidirectionally by defining 'address' for example on your parent's vue data hook:
// parent's data() function
data = function () {
return {
address: {}
}
}
// parent template, passed address with .sync modifier (to make it bi-directional)
<parent-component>
<address-lookup :address.sync='address'></address-lookup>
</parent-component>
// have the props accepted in the address look up component
var addressComponent = Vue.extend({
props: ['address']
})
Now in your $.ajax success function, simply set the props you need on this.address. Of course you can do this with all the props you need: errors, results, state etc. Even better, if you can nest them into a single key on the parent, you can pass the single key for the object containing all four elements instead of all four separately.