ineffective computed loop in vuejs - vue.js

I've got array as below:
return {
items: [
{ active: false, text: 'text1', textOutput: ''},
{ active: false, text: 'text1', textOutput: ''},
{ active: false, text: 'text1', textOutput: ''},
...
],
}
now, the purpose of this array is to output data to the DOM, using computed property as below.
I want to output only the data where active: true, which is changing onclick in other part of the web, so computed resultDOM is watching for this change and changing only items.active="true". Also I use text-fields to change items.text[i] values.
computed: {
resultDOM () {
for (var i=0; i<this.items.length; i++) {
if (this.items[i].active) {
this.items[i].textOutput = '<li>' + this.items[i].text + '</li>'
console.log('bam')
}
else {
this.items[i].textOutput = ''
console.log('bam else')
}
}
var output=''
for (var i=0; i<this.items.length; i++) {
output = output + this.items[i].textOutput
}
return output
}
}
The problem is that this takes some time to execute and every time I change only one active to true (or items.text value) computed is doing check on every element of the table, so it's very ineffective.
Can I ask you for tips, how should I improve my code to be more efficient?
What is the right way to do it?
edit:
<div v-html="resultDOM"></div>

You can avoid using computed at all if you take advantage of conditional rendering and v-for directive:
const app = new Vue({
el: "#app",
data: {
items: [
{ active: true, text: 'text1',},
{ active: true, text: 'text2',},
{ active: false, text: 'text3',},
],
},
})
<script src="https://unpkg.com/vue"></script>
<div id="app">
<ul>
<li v-for="item in items" v-if="item.active" v-text="item.text"></li>
</ul>
</div>
jsfiddle

Related

How to remove active classes from all buttons before making another one active?

I'm new to vue, I still don't understand everything, tell me. I have buttons that I display through v-for, I need to get the active class of only one button when pressed, all the others need to be turned off, tell me, preferably visually, how can I do it better?
I am using the method activeBtn, but this doesn't turn off the active class from the previous buttons
activeBtn(event, index) {
this.buttons[index].isActive = !this.buttons[index].isActive;
<script>
data() {
return {
buttons: [
{
label: "A",
isActive: false,
type: "border-left",
name: "BorderLeftComonent",
},
{
label: "A",
isActive: false,
type: "text-balloon",
name: "TextBalloonComponent"
},
{
label: "A",
isActive: false,
type: "dashed",
name: "DashedComponent"
},
],
};
},
methods: {
activeBtn(event, index) {
this.buttons[index].isActive = !this.buttons[index].isActive;
}
</script>
<template>
<div id="btn-box">
<button
v-for="(button, index) in buttons"
:key="index"
:class="button.isActive ? 'on' : 'off'"
#click="component = button.name, activeBtn($event, index)">
<div :class="`btn btn-${button.type}`">{{ button.label }}</div>
</button>
</div>
</template>
Since you only want to get one active button at any one point, it doesn't make sense to manage the active state inside each button.
Instead, you should manage it at group level by storing the currently selected button's id. A button would then be active when its id matches the currently selected id.
Here's an example:
new Vue({
el: '#app',
data: () => ({
buttons: [
{
id: "button-1",
label: "A",
type: "border-left",
name: "BorderLeftComonent"
},
{
id: "button-2",
label: "A",
type: "text-balloon",
name: "TextBalloonComponent"
},
{
id: "button-3",
label: "A",
type: "dashed",
name: "DashedComponent"
}
],
activeButtonId: "button-1"
}),
methods: {
activate(id) {
this.activeButtonId = id;
}
}
})
.on {
background-color: red
}
.off {
background-color: blue
}
.on, .off {
color: white
}
<script src="https://unpkg.com/vue#2/dist/vue.min.js"></script>
<div id="app">
<div>
<button
v-for="{id, type, label} in buttons"
:key="id"
:class="activeButtonId === id ? 'on' : 'off'"
#click="activate(id)"
>
<div :class="`btn btn-${type}`" v-text="label" />
</button>
</div>
</div>

Vue.js: Including same instance of component multiple times in page

What I am trying to accomplish:
I have some filters that display on a page to filter the products that display on the page. In mobile, I want to hide these filters behind a button that, once pressed, will show the filters in a slide out menu from the side.
While I can duplicate the same components on the page twice, the components are not the exact same instance, that is, clicking on a filter will trigger that function to filter the products on the page, but it sets its own data attributes, which I am using to say "if data attribute 'selected' is true, add a 'selected' class to the component. When I resize the window, the other instance of the component does not have the 'selected' data attribute marked as 'true'.
I expect this, because, from the docs:
Notice that when clicking on the buttons, each one maintains its own, separate count. That’s because each time you use a component, a new instance of it is created.
...but what would be the best way to do this?
I played around with the idea of just setting a class 'mobile' on the component, and the .mobile css would style the components differently, but I need for it to break out where it is nested.
e.g.
<body>
<header>
<!-- desktop -->
<guitar-filters>
<header>
<!-- mobile -->
<guitar-filters>
</body
Here is my Vue component 'guitar-filters' that displays several components called 'instrument-filter':
Vue.component('guitar-filters', {
data: function() {
return {
isMobile: false
}
},
mounted: function() {
var comp = this;
this.setIsMobile();
window.addEventListener('resize', function() {
comp.setIsMobile();
});
},
methods: {
setIsMobile: function() {
this.isMobile = (window.innerWidth <= 900) ? true : false;
}
},
template: `
<ul class="filters" :class="{mobile: isMobile}">
<li>
All
</il>
<li>
Series
<ul>
<instrument-filter filter-by="series" filter="All">All</instrument-filter>
<instrument-filter filter-by="series" filter="Frontier">Frontier</instrument-filter>
<instrument-filter filter-by="series" filter="Legacy">Legacy</instrument-filter>
<instrument-filter filter-by="series" filter="USA">USA</instrument-filter>
</ul>
</li>
<li>
Body Shape
<ul>
<instrument-filter filter-by="bodyType" filter="All">All</instrument-filter>
<instrument-filter filter-by="bodyType" filter="Concert">Concert</instrument-filter>
<instrument-filter filter-by="bodyType" filter="Concertina">Concertina</instrument-filter>
<instrument-filter filter-by="bodyType" filter="Concerto">Concerto</instrument-filter>
<instrument-filter filter-by="bodyType" filter="Orchestra">Orchestra</instrument-filter>
</ul>
</li>
</ul>
`
});
The instrument-filter component:
Vue.component('instrument-filter', {
data: function() {
return {
selected: false
}
},
props : [
'filterBy',
'filter'
],
methods: {
addFilter: function() {
this.$root.$emit('addFilter',{filterBy: this.filterBy,filter: this.filter});
},
clearFilter: function() {
this.$root.$emit('clearFilter',{filterBy: this.filterBy,filter: this.filter});
}
},
template: `
<li :class="{ 'selected' : selected }" #click="selected = !selected; selected ? addFilter() : clearFilter()"><slot></slot></li>
`
});
.css:
ul.filters > li > ul > li.selected::before {
content: "✔️";
...
}
The goal is to have a filter have the 'selected' class in both instances. If I click on 'concert' body shape, and then resize the window to mobile breakpoint, the other instance of that filter component will be selected also.
EDIT: I could hack this. I could move one instance of the component with javascript, but I'm learning Vue, and want to do this the Vue way and best practices.
There's a number of different ways you can handle this. It looks like you've started down the event bus path. Another option could be to use shared app state (see Vuex).
What I've done is similar to shared state, but just using app (same would apply to a common parent component) data. The shared object is passed to both instances of the component. If an item is selected, the appropriate entry is toggled. Since the object is shared, both components stay in sync.
If there was no common parent component, you'd have to look at events or state.
Take a look and see if that helps.
Vue.component('guitar-filters', {
props: [ 'data' ],
data: function() {
return {
isMobile: false
}
},
mounted: function() {
var comp = this;
this.setIsMobile();
window.addEventListener('resize', function() {
comp.setIsMobile();
});
},
methods: {
setIsMobile: function() {
this.isMobile = (window.innerWidth <= 900) ? true : false;
}
},
template: `
<ul class="filters" :class="{mobile: isMobile}">
<li>
All
</il>
<li>
Series
<instrument-filters :list="data.seriesFilters"/>
</li>
<li>
Body Shape
<instrument-filters :list="data.bodyFilters"/>
</li>
</ul>
`
});
Vue.component('instrument-filters', {
props : [ 'list', ],
methods: {
toggle(toggleItem) {
let itemInList = this.list.find((item) => item.value === toggleItem.value);
itemInList.selected = !itemInList.selected;
},
},
template: `
<ul>
<li v-for="item in list" :class="{ 'selected' : item.selected }" #click="toggle(item)">{{ item.label }}</li>
</ul>
`
});
new Vue({
el: "#app",
data: {
filterData: {
seriesFilters: [
{ label: 'All', value: 'All', selected: false },
{ label: 'Frontier', value: 'Frontier', selected: false },
{ label: 'Legacy', value: 'Legacy', selected: false },
{ label: 'USA', value: 'USA', selected: false },
],
bodyFilters: [
{ label: 'All', value: 'All', selected: false },
{ label: 'Concert', value: 'Concert', selected: false },
{ label: 'Concertina', value: 'Concertina', selected: false },
{ label: 'Concerto', value: 'Concerto', selected: false },
{ label: 'Orchestra', value: 'Orchestra', selected: false },
],
}
},
});
ul {
margin-left:20px;
}
ul > li {
cursor: pointer;
}
ul.filters > li > ul > li.selected::before {
content: "✔️";
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<header>
<!-- desktop -->
<guitar-filters :data="filterData" />
</header>
<!-- mobile -->
<guitar-filters :data="filterData" />
</div>
Fiddle: https://jsfiddle.net/nigelw/jpasfkxb

How to $watch property of a list item in VueJS

How can I $watch changes to specific properties of a list item? For instance in the below code, I want to know whenever the Done property on any of the TODO list items changes.
I see from the docs that I can watch subproperties of objects, like myObjects.done in the code below, but I am not sure about the syntax for lists.
I should also mention I would prefer to $watch the data instead of putting event handlers in the UI, and function calls in any spot that changes the property
var vm = new Vue({
el: "#app",
data: {
myObject: { done: true },
todos: [
{ text: "Learn JavaScript", done: false },
{ text: "Learn Vue", done: false },
{ text: "Play around in JSFiddle", done: true },
{ text: "Build something awesome", done: true }
]
},
});
//This works wonderfully on non list items
vm.$watch("myObject.done", function(val)
{
console.log("myObject.done changed", val);
});
//How do I monitor changes to the done property of any of the todo items?
vm.$watch("todos[*].done", function(val)
{
console.log("todos.done changed", val);
})
JSFiddle here: http://jsfiddle.net/eywraw8t/376544/
With your current approach, you'd have to deep-watch the array and do some heavy computations in order to figure out the changed element. Check this link for the example:
Vue - Deep watching an array of objects and calculating the change?
I think the better approach would be using change event handler:
<input type="checkbox" v-model="todo.done" #change="onTodoChange(todo, $event)">
JSFiddle: http://jsfiddle.net/47s0obuc/
To watch specific property, I'd create another component for the list item and pass the item as value to watch the changes from that component.
Vue.component("TaskItem", {
template: `
<li
class="task-item"
:class="{ done: complete }"
>
<p>{{ task.description }}</p>
<input type="checkbox" v-model="complete">
</li>
`,
props: ["task"],
computed: {
complete: {
set(done) {
this.$emit("complete", this.task, done);
// we force update to keep checkbox state synced
// in case if task.done was not toggled by parent component
this.$forceUpdate();
},
get() {
return this.task.done;
}
}
}
});
new Vue({
el: "#app",
template: `
<div>
<ul class="task-list">
<TaskItem
v-for="(task, i) in tasks"
:key="i"
:task="task"
#complete="complete"
/>
</ul>
<button #click="completeFirstTask">Complete first task</button>
</div>
`,
data() {
return {
tasks: [
{ description: "Get milk", done: false },
{ description: "Barber shop", done: true },
{ description: "Fix sleep cycle", done: false }
]
};
},
methods: {
complete(item, done) {
item.done = done;
},
completeFirstTask() {
this.tasks[0].done = true;
}
}
});
https://codesandbox.io/s/wqrp13vp25
I used this and it works for me.
var vm = new Vue({
el: "#app",
data: {
myObject: { done: true },
todos: [
{ text: "Learn JavaScript", done: false },
{ text: "Learn Vue", done: false },
{ text: "Play around in JSFiddle", done: true },
{ text: "Build something awesome", done: true }
]
},
watch:{
todo: function(val) {
console.log ("This TODO is Done", val)
}
});
<template>
<div class="mainDiv" v-for="(index, todo) from todos">
<div>{{todo.text}}</div>
<input type="checkbox" v-model="todo[index].done">
</div>
</template>

Checked item form Array 1, and pass checked item to Array 2, then hide checked item from Array 2 rendered list

First, please check this pen I found, the concept is similar to my question, ReactJS - Baby Name Inspiration. I hope to make it via Vue.js but sorry I don't know React.
The question I want to ask if the user click the list item from Array 1, I named Array 1 as animals, the structure will show below. Then, it will pass the clicked item to Array 2, Array 2 as wished_pets_list. If for example {displayName: "Kitty", value: "cat"} clicked from animals list, animals & wished_pets_list also stored this object. When the same object in two arrays, the render of animals element will hide the object's output in HTML; it also renders to wished_pets_list as button. If click wished_pets_list's item button, it will remove the object data from wished_pets_list, and can access back on animals HTML list. And it can loop again.
The setting of data, default:
data: () => ({
animals: [
{displayName: "Kitty", value: "cat"},
{displayName: "Puppy", value: "dog"},
{displayName: "Chick", value: "bird"},
{displayName: "Fawn", value: "Deer"},
{displayName: "Joey", value: "Kangaroo"},
{displayName: "Piglet", value: "pig"},
{displayName: "Fry", value: "fish"},
{displayName: "Polliwog", value: "frog"}
],
wished_pets_list: [],
wished_pets_list_formatted: []
}),
Something I try on make it as HTML:
<div v-for="item in wished_pets_list">
<span #click="removeSelected(item.value)">{{item.displayName}}</span>
</div>
<div class="dropdown-list-container">
<div class="dropdown-list" v-for="(item, index) in animals">
<label :for="'givenID' + item.index" #click="pushSelect(item.value)">{{index}}{{item.displayName}}</label>
<input type="checkbox" v-model="wished_pets_list" :value="{'displayName': item.displayName, 'value': item.value}" :id="givenID' + item.index">
</div>
</div>
<!-- a hidden text field to submit the formatted as value only -->
<input type="text" v-model="wished_pets_list_formatted" name="anyName" v-show>
Two methods I think it should use:
methods: {
removeSelected(value){
this.wished_pets_list_formatted.push(value);
},
pushSelect(value){
this.wished_pets_list_formatted.splice(value);
}
},
Thanks, if you can, please make a similar codepen or jsfiddle.
Below is a implementation of the example codepen in Vue(didn't included the search part because I think it's irelevant in this case).
The template:
<div id="app">
<div data-reactroot="">
<main>
<div class="favourites">
<h4>Your Shortlist</h4>
<ul>
<li class="girl" v-for="(animal, index) in wished_pets_list" #click="removeFromList(index)">{{animal.displayName}}</li>
</ul>
<hr>
</div>
<ul>
<li v-for="(animal, index) in animals" :key="animal.value" class="boy" #click="addToList(index)">{{animal.displayName}}</li>
</ul>
</main>
</div>
</div>
The javascript part:
var vm = new Vue({
el: "#app",
data () {
return {
animals: [
{displayName: "Kitty", value: "cat"},
{displayName: "Puppy", value: "dog"},
{displayName: "Chick", value: "bird"},
{displayName: "Fawn", value: "Deer"},
{displayName: "Joey", value: "Kangaroo"},
{displayName: "Piglet", value: "pig"},
{displayName: "Fry", value: "fish"},
{displayName: "Polliwog", value: "frog"}
],
wished_pets_list: [],
wished_pets_list_formatted: []
}
},
methods: {
addToList(index) {
this.wished_pets_list.push(this.animals[index])
this.animals.splice(index, 1)
},
removeFromList(index) {
this.animals.push(this.wished_pets_list[index])
this.wished_pets_list.splice(index, 1)
}
}
});
For the CSS you can use the one from the codepen example.
Codepen fork
Base on #Allkin's answer, and my addition requirement, I tried to make such like Allkin's answer with an ordered list.
The template:
<div id="app">
<div>
<div class="favourites">
<h4>Your Shortlist</h4>
<ul>
<li class="girl" v-for="(animal, index) in wished_pets_list" #click="removeFromList(index, animal.value, animal.id)">{{animal.displayName}}</li>
</ul>
<hr>
</div>
<ul>
<li v-for="(animal, index) in animals" :key="animal.value" class="boy" #click="addToList(index, animal.value, animal.id)" v-show="!animal.checked">{{animal.displayName}}</li>
</ul>
<span>wished_pets_list_formatted:</span>
<div>{{wished_pets_list_formatted}}</div><br><br>
<span>animals:</span>
<div>{{animals}}</div>
</div>
</div>
js:
var vm = new Vue({
el: "#app",
data() {
return {
org_animal_list: [
{ displayName: "Kitty", value: "cat" },
{ displayName: "Puppy", value: "dog" },
{ displayName: "Chick", value: "bird" },
{ displayName: "Fawn", value: "Deer" },
{ displayName: "Joey", value: "Kangaroo" },
{ displayName: "Piglet", value: "pig" },
{ displayName: "Fry", value: "fish" },
{ displayName: "Polliwog", value: "frog" }
],
animals: null,
wished_pets_list: [],
wished_pets_list_formatted: []
};
},
methods: {
addToList(index, value, id) {
console.log("added: " + value);
this.wished_pets_list.push(this.animals[index]);
this.wished_pets_list_formatted.push(value);
this.animals[index].checked = !this.animals[index].checked;
},
removeFromList(index, value, id) {
var self = this;
this.wished_pets_list.splice(index, 1);
this.wished_pets_list_formatted.forEach(function(item, index) {
if (item == value) {
self.wished_pets_list_formatted.splice(index, 1);
}
});
for (var i = 0; i < this.animals.length; i++) {
if (self.animals[i].id == id) {
self.animals[i].checked = !self.animals[i].checked;
}
}
}
},
beforeMount: function() {
this.animals = this.org_animal_list;
for (var i = 0; i < this.animals.length; i++) {
this.$set(this.animals[i], "checked", false);
this.$set(this.animals[i], "id", i);
}
}
});
I added an original list first, then it will clone before Vue mount. This action allows the developer can use back the original data for other use.
For the full example, please check on codepen

How to delete a dynamically generated form based on the click of the delete button with respect to its ID in vuejs2

I am creating an application using Quasar and VueJS. I am able to generate a dynamic form on click of the add button, but not able to delete any of the newly generated form based on the click of the delete button.Find the code below:
<template>
<div v-for="h in htmlList">
<div v-for="r in h" >
<div v-html="r" v-on:click="useRemoveFromProject(1)" v-bind:id="r.id">
</div>
</div>
</div>
</template>
<script>
/*
* Root component
*/
import Vue from 'vue'
export default {
name: 'q-app',
data () {
return {
flag: 0,
htmlList: [],
select: 'fb',
select1: 'fb1',
multipleSelect: ['goog', 'twtr'],
usersInProject: [],
selectOptions: [
{
label: 'Google',
value: 'goog'
},
{
label: 'Select',
value: 'fb'
},
{
label: 'Twitter',
value: 'twtr'
},
{
label: 'Apple Inc.',
value: 'appl'
},
{
label: 'Oracle',
value: 'ora'
}
],
selectOptions1: [
{
label: 'Integer',
value: 'goog1'
},
{
label: 'Float',
value: 'fb1'
},
{
label: 'String',
value: 'twtr1'
}
]
}
},
methods: {
useRemoveFromProject: function (id) {
console.log('hi....')
Vue.delete(this.htmlList, id)
},
identifyMe: function (event) {
alert('hi - ' + event.target.id)
},
process: function () {
this.flag += 1
let temp = []
temp.push('<div class="card" id="a_' + this.flag + '"> <div class="card-content content-center "> <large id="l4">Expression LHS:</large> <input><br> <large id="l5">Operators:</large> <q-select type="radio" v-model="this.select" :options="this.selectOptions"></q-select><br><large id="l4">Expression RHS:</large> <input><br><large id="l5">Data type:</large> <q-select type="radio" v-model="select1" :options="selectOptions1"></q-select><br></div><button class="cordova-hide circular red " style="margin-bottom:5px; margin-right:30px;" v-on:click="userRemoveFromProject(i)"><i>delete</i></button><input value="click" type="button"> </div>')
let ids = ['a_' + this.flag]
console.log(ids)
this.htmlList.push(temp)
}
}
}
</script>
After looking to your code i noticed that you have some errors:
Call function useRemoveFromProject without the 'r' of 'user'
Call userRemoveFromProject when clicking on the element and not only the delete button
Call userRemoveFromProject(i) with a 'i' variable, but what is 'i' ?
Why using a double v-for? The first level is enough.
I propose to you a working example on a fiddle. Please let me know if it's useful for you (and mark it as resolve if it's the case).
EDIT: for Vue.js 2 https://jsfiddle.net/86216oko/