Hi guys Im working with Vue, and Im trying to make recursive Tree from Flat list, I would like to toogle expanded property of each item when someone clicks on it, but for some reason it is not changin
It is happening in this function
expandNode(item) {
console.log("HERERERER");
item.expand = false;
this.$set(item, "expand", false);
}
I would like my Array to be reactive but for some reason it is not, Is it maybe the way Im reapacking the data or something else, Could someone take a look ??
Here is my CodeSandbox
https://codesandbox.io/s/condescending-tree-51rbs
this is the component code
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<tr v-for="(item ,index) in flatArray" :key="index">
<div class="item" #click="expandNode(item)">
<div class="element" v-show="item.expand">
{{ item.expand }}
<span>{{ item.label }}</span>
</div>
</div>
</tr>
</div>
</template>
<script>
export default {
name: "HelloWorld",
props: {
msg: String,
data: { default: () => null, type: Array }
},
data() {
return {
flatArray: []
};
},
mounted() {
let arr = [];
console.log("HERERER");
this.recursive(this.data, arr, 0, null, -1);
this.flatArray = arr;
console.log(this.flatArray);
},
computed: {
setPadding(item) {
return `padding-left: ${item.level * 30}px;`;
}
},
methods: {
recursive(obj, newObj, level, parent, parentIndex) {
obj.forEach(node => {
if (node.children && node.children.length != 0) {
node.level = level;
node.leaf = false;
node.expand = true;
node.parent = parent;
node.parentIndex = parent ? parentIndex : null;
newObj.push(node);
this.recursive(
node.children,
newObj,
node.level + 1,
node,
newObj.indexOf(node)
);
} else {
node.level = level;
node.leaf = true;
node.expand = true;
node.parent = obj;
node.parentIndex = parent ? parentIndex : null;
newObj.push(node);
return false;
}
});
},
expandNode(item) {
console.log("HERERERER");
item.expand = false;
this.$set(item, "expand", false);
}
}
};
</script>
the reason you're not seeing an update is that the array has no reason to recalculate. You're updating this.$set(item, "expand", false); an object that is not reactive. The reason it's not reactive is because you're not using $set method when you're creating the object.
here is what it would look like if you use $set correctly during object creation.
recursive(obj, newObj, level, parent, parentIndex) {
obj.forEach(node => {
this.$set(node, "level", level);
this.$set(node, "expand", true);
this.$set(node, "parentIndex", parent ? parentIndex : null);
if (node.children && node.children.length !== 0) {
this.$set(node, "leaf", false);
this.$set(node, "parent", parent);
newObj.push(node);
this.recursive(
node.children,
newObj,
node.level + 1,
node,
newObj.indexOf(node)
);
} else {
this.$set(node, "leaf", true);
this.$set(node, "parent", obj);
newObj.push(node);
return false;
}
});
},
note that you can now use item.expand = false
expandNode(item) {
item.expand = false;
// this.$set(item, "expand", false); <== not needed
}
you can see in action here
Alternatively,...
Here is code that might work for you that doesn't rely on reactivity
Note that:
I'm re-calculating and re-assigning the array with this.flatArray = flattenTree(this.data);
The idea is that the nested objects are a "source of truth", and the flattened array is there to allow the template to render.
<template>
<div class="hello">
<tr v-for="(item ,index) in flatArray" :key="index">
<div
#click="toggleExpandNode(item)"
class="item"
:style="{'margin-left':item.level * 1.6 +'em'}"
>
<div class="element">
<span v-if="item.leaf">⚬</span>
<span v-else-if="item.expand">▾</span>
<span v-else>▸</span>
<span>{{ item.label }}</span>
</div>
</div>
</tr>
</div>
</template>
<script>
const flattenTree = obj => {
const flatTreeArr = [];
let depth = 0;
const flatten = (node, parentNode) => {
flatTreeArr.push(node);
node.level = depth;
node.leaf = true;
node.parent = parentNode;
node.expand = node.expand === undefined ? true : node.expand;
if (node.children) {
node.leaf = false;
if (node.expand) {
depth++;
node.children.forEach(br => flatten(br, node));
depth--;
}
}
};
obj.forEach(br => flatten(br, null));
return flatTreeArr;
};
export default {
props: {
data: { default: () => null, type: Array }
},
data() {
return {
flatArray: []
};
},
mounted() {
this.flatArray = flattenTree(this.data);
},
methods: {
toggleExpandNode(item) {
item.expand = !item.expand;
this.flatArray = flattenTree(this.data);
}
}
};
</script>
see it in action here
If item object has no initial expand property, you must declare it as observable using this.$set(item, ...).
Directly adding new property like item.expand = ... skips this step and this.$set ignores it. Such property will not be reactive.
More about this in here: https://v2.vuejs.org/v2/guide/reactivity.html
Related
I have a search function that filters an array if any one of the tags in the search tags array are included in the item's tags array. Instead of using some() to return true if any tag is present, how can I return true if every tag is present?
Here's an example of what I have (which works). If I search "tag1;" and "tag3;", it returns all 3 items. What I want is only item1 and item2 to return true.
<template>
<div class="right-content">
<div class="nav-margin">
<p>Filter array</p>
<input type="text" v-model="searchTag" #keyup="addSearchTag" />
<div class="spacer-20"></div>
<div v-for="searchTag in searchTags" :key="searchTag" class="items">{{searchTag}}</div>
<div class="spacer-20"></div>
<div v-for="item in searchedItems" :key="item.name" class="items">
<div class="title">{{item.name}}:</div>
<div v-for="tag in item.tags" :key="tag" class="tag">{{tag}}</div>
</div>
</div>
</div>
</template>
<script>
import { ref } from "#vue/reactivity";
import { computed } from '#vue/runtime-core';
export default {
name: "TemplatesDash",
setup() {
const searchTag = ref("");
const searchTags = ref([])
const filteredItems = ref(null)
let items = [
{ name: "item1", tags: ["tag1;", "tag2;", "tag3;"] },
{ name: "item2", tags: ["tag1;", "tag3;"] },
{ name: "item3", tags: ["tag2;", "tag3;"] },
];
const addSearchTag = (e) => {
if (e.key === ";" && searchTag.value) {
if (!searchTags.value.includes(searchTag.value)) {
searchTags.value.push(searchTag.value);
}
searchTag.value = "";
}
};
const searchedItems = computed(() => {
filteredItems.value = items;
if (searchTags.value.length) {
filteredItems.value = filteredItems.value.filter((item) => {
return item.tags.some(
(r) => searchTags.value.indexOf(r) !== -1
);
});
}
return filteredItems.value
});
return { searchTag, searchedItems, searchTags, addSearchTag, };
},
};
</script>
I'm brand new to javascript so any help or pointing in the right direction would be great.
Thanks
I believe the .every() function may be useful here. When you filter the items in your searchedItems computed function, return an .every() call on searchTags, returning whether item.tags .includes() every searchTag array member.
const searchedItems = computed(() => {
filteredItems.value = items;
if (searchTags.value.length) {
filteredItems.value = filteredItems.value.filter((item) => {
return searchTags.value.every(t => item.tags.includes(t));
});
}
return filteredItems.value
});
You're off to a great start with javascript! Hope this helps!
I want to update the todoList in my PARENT COMPONENT after I have added a new item in my child using the AddItem() method. Nothing gets added the first time.
EX. if I add "take test" doesn't get render, then if I add "take shower" doesn't get rendered but now "take test" does. Then if I add "take a leak" "take shower" gets rendered.
PARENT COMPONENT
firstUpdated(changedProperties) {
this.addEventListener('addItem', e => {
this.todoList = e.detail.todoList;
});
}
render() {
return html`
<p>Todo App</p>
<add-item></add-item>//Child item that triggers the add
<list-items todoList=${JSON.stringify(this.todoList)}></list-items>
`;
}
CHILD COMPONENT
AddItem() {
if (this.todoItem.length > 0) {
let storedLocalList = JSON.parse(localStorage.getItem('todo-list'));
storedLocalList = storedLocalList === null ? [] : storedLocalList;
const todoList = [
...storedLocalList,
{
id: this.uuidGenerator(),
item: this.todoItem,
done: false
}
];
localStorage.setItem('todo-list', JSON.stringify(todoList));
this.dispatchEvent(
new CustomEvent('addItem', {
bubbles: true,
composed: true,
detail: { todoList: storedLocalList }
})
);
this.todoItem = '';
}
}
render() {
return html`
<div>
<input .value=${this.todoItem} #keyup=${this.inputKeyup} />
<button #click="${this.AddItem}">Add Item</button>
</div>
`;
}
You need to set properties for todoItem
static get properties() {
return {
todoItem: {
type: Array,
Observer: '_itemsUpdated'
}
}
constructor(){
this.todoItem = []
}
_itemsUpdated(newValue,oldValue){
if(newValue){
-- write your code here.. no event listeners required
}
}
In above code., We need to initialise empty array in constructor.
Observer observe the changes to array & triggers itemsUpdated function which carries oldValue & NewValue. In that function., you can place your logic.
No Event Listeners required as per my assumption
Found my error. I was passing to detail: { todoList : storedLocalList } which is the old array without the updated value.
AddItem() {
if (this.todoItem.length > 0) {
let storedLocalList = JSON.parse(localStorage.getItem('todo-list'));
storedLocalList = storedLocalList === null ? [] : storedLocalList;
const todoList = [
...storedLocalList,
{
id: this.uuidGenerator(),
item: this.todoItem,
done: false
}
];
localStorage.setItem('todo-list', JSON.stringify(todoList));
this.dispatchEvent(
new CustomEvent('addItem', {
bubbles: true,
composed: true,
detail: { todoList: todoList }
})
);
this.todoItem = '';
}
}
i try to remove a object from array in vueJS but this is impossible.
I tyr a lot of thing and read some solution on stackoverflow but nothing work for me.
I have a fake list like this in my vue.html component :
<div class="custo-list-c">
<div v-for="(item, index) in valuesFounded"
#click="addItem(item)"
v-bind:class="{ 'selected': itemSelected(item) }">
{{ item.value }}
<span v-if="itemSelected(item)">
<i class="fa fa-remove" #click="itemDeleted(item)"></i>
</span>
</div>
</div>
And my component look something like this :
import Vue from 'vue';
import Component from 'vue-class-component';
import { Prop, Watch, Emit } from "vue-property-decorator";
#Component({
name: 'custolist-component',
template: require('./custo-list.component.vue.html'),
components: {}
})
export default class CustoListComponent extends Vue {
public custoListActive: boolean = false;
public valuesFounded: Array<{key: string, value: string}> = [];
public selectedItems_: Array<{key: string, value: string}> = [];
#Prop() list;
#Watch('list') onListChanged(newList, oldList) {
// this.list = newList;
}
#Prop() placeholder;
#Watch('placeholder') onPlaceholderChanged(newPlaceholder, oldPlaceholder) {
// console.log(newPlaceholder);
}
#Prop() disabled;
#Watch('disabled') onDisabledChanged(newDisabled, oldDisabled) {
// console.log(newPlaceholder);
}
public open(event) {
this.custoListActive = true;
if (!event.target.value) {
this.valuesFounded = this.list;
} else {
this.valuesFounded = [];
const re = new RegExp(event.target.value, 'ig');
for (var i=0; i<this.list.length; i++) {
if (this.list[i].key.match(re) || this.list[i].value.match(re)) {
this.valuesFounded.push(this.list[i]);
}
}
}
}
public addItem(item: {key: string, value: string}) {
if (!this.isSelectedItem_(item)) {
this.selectedItems_.push(item);
// this.custoListActive = false;
};
this.$emit('itemclicked', item);
}
public itemSelected(item) {
return this.isSelectedItem_(item);
}
public itemDeleted(item) {
for (var i=0; i<this.selectedItems_.length; i++) {
if (item.key == this.selectedItems_[i].key) {
this.selectedItems_.splice(i, 1);
break;
}
}
this.$emit('itemdeleted', item);
}
private isSelectedItem_(item) {
const filtered = this.selectedItems_.filter(m => {
return m.key == item.key;
});
return filtered.length > 0;
}
}
but when i do this.selectedItems_.splice(i, 1); that does not work !!
Thank for your help
More precisions about my code. Here the method where i remove item from my array :
public itemDeleted(item) {
const filtered = this.selectedItems_.filter(m => {
return m.key != item.key;
});
console.log(filtered, this.selectedItems_.length);
this.selectedItems_ = filtered;
console.log(this.selectedItems_, this.selectedItems_.length);
this.$emit('itemdeleted', item);
}
And the result in the console
console
What's wrong?
Another test :
public itemDeleted(item) {
this.selectedItems_ = this.selectedItems_.filter(m => {
return m.key != item.key;
});
this.selectedItems_.splice(this.selectedItems_.length);
console.log(this.selectedItems_, this.selectedItems_.length);
this.selectedItems_ = [];
console.log(this.selectedItems_, this.selectedItems_.length);
this.$emit('itemdeleted', item);
}
result :
console
May be a bug VueJS
Sorry, it was my fault, replace
<i class="fa fa-remove" #click="itemDeleted(item)"></i>
by
<i class="fa fa-remove" v-on:click.stop="itemDeleted(item)"></i>
Having multiple steps of finding value with combination of for loop and if statement reduces readability and code predictability. In addition, invoking array mutating method splice may not trigger reactive update of this property.
I'd suggest to use filter and re-assign selectedItems_ inside itemDeleted method as follows:
public itemDeleted(item) {
this.selectedItems_ = this.selectedItems_.filter(selectedItem => selectedItem.key !== item.key)
this.$emit('itemdeleted', item);
}
This way, after method execution, selectedItems_ will consist of all previous items except the one provided as an argument to the method and all dependent properties will be re-computed.
I have this 3 components in VueJS. The problem i want to solve is: When i click at vehicle component, it needs to be selected (selected = true) and other vehicles unselected.
What i need to do for two-way data binding? Because i'm changing this selected property in VehiclesList.vue component and it also need to be changed in Monit.vue (which is a parent) and 'Vehicle.vue' need to watch this property for change class.
Also problem is with updating vehicles. In Monit.vue i do not update full object like this.vehicles = response.vehicles, but i do each by each one, and changing only monit property.
Maybe easier would be use a store for this. But i want to do this in components.
EDITED:Data sctructure
{
"m":[
{
"id":"v19",
"regno":"ATECH DOBLO",
"dt":"2017-10-09 13:19:01",
"lon":17.96442604,
"lat":50.66988373,
"v":0,
"th":0,
"r":0,
"g":28,
"s":"3",
"pow":1
},
{
"id":"v20",
"regno":"ATECH DUCATO_2",
"dt":"2017-10-10 01:00:03",
"lon":17.96442604,
"lat":50.6698494,
"v":0,
"th":0,
"r":0,
"g":20,
"s":"3"
},
]
}
Monit.vue
<template>
<div class="module-container">
<div class="module-container-widgets">
<vehicles-list :vehicles="vehicles"></vehicles-list>
</div>
</div>
</template>
<script>
import VehiclesList from '#/components/modules/monit/VehiclesList.vue';
export default {
name: "Monit",
data (){
return {
vehicles: null
}
},
components: {
VehiclesList
},
methods: {
getMonitData(opt){
let self = this;
if (this.getMonitDataTimer) clearTimeout(this.getMonitDataTimer);
this.axios({
url:'/monit',
})
.then(res => {
let data = res.data;
console.log(data);
if (!data.err){
self.updateVehicles(data.m);
}
self.getMonitDataTimer = setTimeout(()=>{
self.getMonitData();
}, self.getMonitDataDelay);
})
.catch(error => {
})
},
updateVehicles(data){
let self = this;
if (!this.vehicles){
this.vehicles = {};
data.forEach((v,id) => {
self.vehicles[v.id] = {
monit: v,
no: Object.keys(self.vehicles).length + 1
}
});
} else {
data.forEach((v,id) => {
if (self.vehicles[v.id]) {
self.vehicles[v.id].monit = v;
} else {
self.vehicles[v.id] = {
monit: v,
no: Object.keys(self.vehicles).length + 1
}
}
});
}
},
},
mounted: function(){
this.getMonitData();
}
};
</script>
VehiclesList.vue
<template>
<div class="vehicles-list" :class="{'vehicles-list--short': isShort}">
<ul>
<vehicle
v-for="v in vehicles"
:key="v.id"
:data="v"
#click.native="select(v)"
></vehicle>
</ul>
</div>
</template>
<script>
import Vehicle from '#/components/modules/monit/VehiclesListItem.vue';
export default {
data: function(){
return {
isShort: true
}
},
props:{
vehicles: {}
},
methods:{
select(vehicle){
let id = vehicle.monit.id;
console.log("Select vehicle: " + id);
_.forEach((v, id) => {
v.selected = false;
});
this.vehicles[id].selected = true;
}
},
components:{
Vehicle
}
}
</script>
Vehicle.vue
<template>
<li class="vehicle" :id="data.id" :class="classes">
<div class="vehicle-info">
<div class="vehicle-info--regno font-weight-bold"><span class="vehicle-info--no">{{data.no}}.</span> {{ data.monit.regno }}</div>
</div>
<div class="vehicle-stats">
<div v-if="data.monit.v !== 'undefined'" class="vehicle-stat--speed" data-name="speed"><i class="mdi mdi-speedometer"></i>{{ data.monit.v }} km/h</div>
</div>
</li>
</template>
<script>
export default {
props:{
data: Object
},
computed:{
classes (){
return {
'vehicle--selected': this.data.selected
}
}
}
}
</script>
Two-way component data binding was deprecated in VueJS 2.0 for a more event-driven model: https://v2.vuejs.org/v2/guide/components.html#One-Way-Data-Flow
This means, that changes made in the parent are still propagated to the child component (one-way). Changes you make inside the child component need to be explicitly send back to the parent via custom events: https://v2.vuejs.org/v2/guide/components.html#Custom-Events or in 2.3.0+ the sync keyword: https://v2.vuejs.org/v2/guide/components.html#sync-Modifier
EDIT Alternative (maybe better) approach:
Monit.vue:
<template>
<div class="module-container">
<div class="module-container-widgets">
<vehicles-list :vehicles="vehicles" v-on:vehicleSelected="onVehicleSelected"></vehicles-list>
</div>
</div>
</template>
<script>
import VehiclesList from '#/components/modules/monit/VehiclesList.vue';
export default {
name: "Monit",
data (){
return {
vehicles: null
}
},
components: {
VehiclesList
},
methods: {
onVehicleSelected: function (id) {
_.forEach((v, id) => {
v.selected = false;
});
this.vehicles[id].selected = true;
}
...other methods
},
mounted: function(){
this.getMonitData();
}
};
</script>
VehicleList.vue:
methods:{
select(vehicle){
this.$emit('vehicleSelected', vehicle.monit.id)
}
},
Original post:
For your example this would probably mean that you need to emit changes inside the select method and you need to use some sort of mutable object inside the VehicleList.vue:
export default {
data: function(){
return {
isShort: true,
mutableVehicles: {}
}
},
props:{
vehicles: {}
},
methods:{
select(vehicle){
let id = vehicle.monit.id;
console.log("Select vehicle: " + id);
_.forEach((v, id) => {
v.selected = false;
});
this.mutableVehicles[id].selected = true;
this.$emit('update:vehicles', this.mutableVehicles);
},
vehilcesLoaded () {
// Call this function from the parent once the data was loaded from the api.
// This ensures that we don't overwrite the child data with data from the parent when something changes.
// But still have the up-to-date data from the api
this.mutableVehilces = this.vehicles
}
},
components:{
Vehicle
}
}
Monit.vue
<template>
<div class="module-container">
<div class="module-container-widgets">
<vehicles-list :vehicles.sync="vehicles"></vehicles-list>
</div>
</div>
</template>
<script>
You still should maybe think more about responsibilities. Shouldn't the VehicleList.vue component be responsible for loading and managing the vehicles? This probably would make thinks a bit easier.
EDIT 2:
Try to $set the inner object and see if this helps:
self.$set(self.vehicles, v.id, {
monit: v,
no: Object.keys(self.vehicles).length + 1,
selected: false
});
I'm learning Due and I'm trying something that should be easy but doesn't work and I'm sure there is something I don't understand.
I have to add a case to a modal...
I simplify the code: I have an array of products from my parent passed the a props and to chunk them in columns I use a computed variabile.
In the computed variable I also add to my object array an attribute active for every object, and I need to use that attribute to add the class.
I cannot change the value: when I click the button the product.active value is changed if I look the console but in my template no, it is false. Why
<template>
<div class="columns" v-for="products in processedProducts">
<div class="column" v-for="product in products">
<pre>{{product.active}}</pre>
<a v-on:click="activeteModal(product)">Pricy History</a>
<price-history :asin="product.asin" :active="product.active"></price-history>
</div>
</div>
</template>
<script>
import PriceHistory from '../components/PriceHistory'
export default {
props: ['results','search','maxprice','discount'],
name: 'product',
components: {
PriceHistory
},
methods: {
activeteModal: function(product){
console.log(product.active);
product.active = !product.active;
console.log(product.active);
}
},
computed: {
processedProducts() {
let products = this.results.map((obj) => {
obj.active = false;
return obj;
})
// Put Array into Chunks
let i, j, chunkedArray = [], chunk = 5;
for (i=0, j=0; i < products.length; i += chunk, j++) {
chunkedArray[j] = products.slice(i,i+chunk);
}
return chunkedArray;
}
}
}
</script>
Computed objects update lazy, set the array as a data property then it will update reactive.
Furthermore computed-objects are by default getter-only.
You better trigger a method that fills the product array when the component is mounted like this:
export default {
props: ['results','search','maxprice','discount'],
name: 'product',
components: {
PriceHistory
},
data: function () {
return {
products :[]
}
},
mounted: function(){
this.processedProducts();
},
methods: {
activeteModal: function(product){
console.log(product.active);
product.active = !product.active;
console.log(product.active);
},
processedProducts() {
let products = this.results.map((obj) => {
obj.active = false;
return obj;
})
// Put Array into Chunks
let i, j, chunk = 5;
for (i=0, j=0; i < products.length; i += chunk, j++) {
this.products[j] = products.slice(i,i+chunk);
}
}
}
}