VueJs: Working with v-class - javascript

I am trying to assign a css class to a span element, whenver it push the EDIT button.
This is my working example in jsfiddle:
http://jsfiddle.net/r3nepL7u/
BUT it only works, because I check if the title property of the edited object is equal to the title property of the todo object, instead I'd rather check if the two objects are equal.
Unfortunately this breaks my code, whenever I have the same property (e.g. titles) but different objects.
<td>
<span v-class="
completed: todo.completed,
editing: editedTodo.title == todo.title">
{{ todo.title }}
</span>
</td>
Instead I would like to do something like this, where I check todo == editedTodo
<span v-class="
completed: todo.completed,
editing: editedTodo == todo">
{{ todo.title }}
</span>
Non Working Jsfiddle:
http://jsfiddle.net/r3nepL7u/1/
How do I check if todo is equal with editedTodo. AND is there a better way, to use the v-class directive, instead of using inline expressions, meaning for more complicated calculations?
It seems to work fine in the todomvc example here:
Line 23: https://github.com/yyx990803/vue/blob/dev/examples/todomvc/index.html

Conditional class names in Vue.js
Here is how you can do it based on the documentation
1st way
as boolean variable
You define a boolean variable in your js file and based on that it will set the class
js file
data: {
isActive: true,
hasError: false
}
html file
<div class="static"
v-bind:class="{ active: isActive, 'text-danger': hasError }">
</div>
as object
you can also define an object with class names
js file
data: {
classObject: {
active: true,
'text-danger': false
}
}
html file
<div v-bind:class="classObject"></div>
2nd way
You define the "name of the class" variable in your js file and based on the nameOfTheClass it will set the class
js file
data: {
nameOfTheClass: 'this-is-the-name-of-the-class'
}
html file
<div v-bind:class="nameOfTheClass"></div>
3rd way
You can set the name of the class in js and then evaluate with if statement in html file
js file
data: {
nameOfTheClass: 'this-is-the-name-of-class'
}
html file
<div v-bind:class="{ active: nameOfTheClass === 'this-is-the-name-of-class'}">

Add a method to your View Model that does a deep comparison. For instance, create a method called todoIsEqual and then have it use LoDash to do the comparison:
[...]
methods: {
todoIsEqual: function (todo_a, todo_b) {
return _.isEqual(todo_a, todo_b);
}
[...]
and use it like this:
<span v-class="
completed: todo.completed,
editing: todoIsEqual(editedTodo, todo)">
{{ todo.title }}
</span>

Actually the reason it didn't work was pretty simple:
I falsely assinged just two properties and made an if statement to to see if the two objects are equal. I did this:
editTask: function (that) {
this.editedTodo = {
body: that.todo.body,
completed: that.todo.completed
};
},
Instead of asigning the actual object to the editedTodo, like this:
editTask: function (that) {
this.editedTodo = that.todo;
},
Problem solved.

Related

Force change detection inside template for an object part of an array of objects inside an array of objects

Whatever I try, I just cannot seem to trigger change detection inside the template for an object that is part of an array of objects inside an array of objects. This is the (top) object structure:
export interface CompatibleCards {
appid: number,
banned: boolean,
items: { addedToTrade: boolean, appPosition: number, banned: boolean, classid: number, iconUrl: string, marketHashName: string, name: string, numberOfListings: number, position: number, price: number, quantity: number, type: string }[],
name: string,
numberOfCardsInBadge: number,
numberOfDifferentCards: number,
totalNumberOfItems: number
}
And here is the template:
<ng-container *ngIf="this.compatibleCards?.length else no_compatible_cards">
<h3>{{ numberOfCompatibleCards }} cartes compatibles</h3>
<div class="conteneur-ensembles">
<div *ngFor="let currentApp of compatibleCards" class="conteneur-ensemble-compatible">
<h3>{{ currentApp.name }} ({{ currentApp.numberOfDifferentCards }} / {{ currentApp.numberOfCardsInBadge }} cards)</h3>
<div class="conteneur-images-cartes">
<span *ngFor="let currentItem of currentApp.items" title="{{ currentItem.name }}" [ngClass]="{ 'conteneur-carte': true, 'ajoute-echange': currentItem.addedToTrade }" (click)="ajouterRetirerObjetPourEchange(currentItem, 'ajouter')">
<img class="image-carte" src="http://cdn.steamcommunity.com/economy/image/{{ currentItem.iconUrl }}" alt="{{ currentItem.name }}" title="{{ currentItem.addedToTrade }}">
<span *ngIf="currentItem.price && currentItem.price > 0" class="prix-carte" title="{{ currentItem.name }}">{{ currentItem.price / 100 | currency }}</span>
<span *ngIf="currentApp.banned" class="prix-carte" title="{{ currentItem.name }}">--</span>
</span>
</div>
</div>
</div>
</ng-container>
As you can see, I am using the [ngClass] directive to populate the css classes based on the addedToTrade property of the CompatibleCards.items object. It populates properly, meaning that if I change the addedToTrade property before the first render, the class is present. However, when I try to update the object at a later time, using the code below, the template does not show or remove the class.
ajouterRetirerObjetPourEchange(objetAEchanger: any, operation : "ajouter" | "retirer") : void {
console.log(objetAEchanger);
if(operation == "ajouter") {
this.itemsForTrade.push({...objetAEchanger});
} else {
const positionObjetEchangeTrouve = this.itemsForTrade.findIndex((currentItem: any) => currentItem.classid == objetAEchanger.classid && currentItem.position == objetAEchanger.position);
this.itemsForTrade.splice(positionObjetEchangeTrouve, 1);
}
this.compatibleCards[objetAEchanger.appPosition].items[objetAEchanger.position].addedToTrade = ! this.compatibleCards[objetAEchanger.appPosition].items[objetAEchanger.position].addedToTrade;
// this.compatibleCards[objetAEchanger.appPosition].items = [].concat(this.compatibleCards[objetAEchanger.appPosition].items);
// this.compatibleCards = [].concat(this.compatibleCards);
}
Even reassigning the arrays with [].concat (or using spread [...this.compatibleCards] does not update the template (see the comments at the end of the function above).
Is there any way I can force my template to update itself after an update of an object inside the items array?
1st Solution
Try forcing a detect changes programatically.
Use in your compnent's constructor (via Injection) changeDetectorRef: ChangeDetectorRef
and then where you update your table use this.changeDetectorRef.detectChanges()
angular doc
2nd Solution
If that does not work then you can also try a 2nd alternative.
You can declare your component to have a different detection strategy changeDetection:ChangeDetectionStrategy.OnPush
This will be applied on component level like
#Component({
selector: 'my-app',
template: `some template code`,
changeDetection: ChangeDetectionStrategy.OnPush
})
Please take care however as this can have side effects. Detection strategy works from top to bottom. This means that if you declare this strategy on your component all of it's children component will follow this strategy also.
Also on this type of strategy Angular does not detect the changes on primitive fields. It detects changes only on references.
So when you do this.compatibleCards = [...this.compatibleCards] you change the reference of that field so it must be caught as change from Angular.
angular detection strategy doc

Vue.js - Best practice to interpolate data value in console.log object inside methods function?

How can I interpolate text string in nested array to console.log()?
Example:
<template>
<div id="myNavigation">
<div
class="button_link"
v-for="(click) in items"
:key="item.click"
>
<div
class="home-navigation-item-component left-panel selected"
#click="click = menuClickNavigation(click)"
>
{{ item.click }} <!-- Should show the data string -->
</div>
</div>
</div>
</template>
<script>
export default {
data: () => ({
items: [
{ click: 'click one' /* I want this to interpolate to console.log */ },
{ click: 'click two' /* This text string should show when a different element is clicked */ }
]
}),
methods: {
menuClickNavigation (click) {
this.click = (this.items.click)
console.log(this.items.click) /* How to interpolate the items.click value? I want it to display "click one" in the console log function. */
},
}
</script>
Console log displays undefined with current code because it's not interpolating to the desired data value text string in the nested array. Changing the interpolation method (which doesn't currently work because I have no idea how to make it work) with a text string, I get whatever is put in the console.log(), but I want the console.log to interpolate the corresponding text strings in the nested array of items.
Does this make sense?
Aren't you trying to access the value "click" from the items array? I think you should use console.log(this.items[0].click).

angular ng-if json array contains boolean

Angular 1 app.
Basically, there is an array of JSON object like this:
ctrl.messages =
[
{id: 1, text: 'hello1',createdBy:{name: 'Jack', hasBeenRead: false} }
,{id: 2, text: 'hello2',createdBy:{name: 'Steven', hasBeenRead: true} }
];
Now, in the view I print the messages like this:
<div ng-repeat="message in messages">
Message: {{message.text}}
</div>
Somewhere else I have this html:
<div ng-if="messages.length > 0">
<button ng-if="?">Mark all read</button>
<button ng-if="?">Mark all unread</button>
</div>
The buttons above will never be showing together. But only one of them (and only if there are messages at all).
I wonder if it possibile to add in the ng-if above (in the button) a code for understanding if the button has to be showing.
The button Mark all read will be showing only if there is at least one message marked with hasBeenRead: false.
The button Mark all unread will be showing only if all the messages have been read.
I could do this in the controller. But I thought it would be neater if I could add this directly in the view.
The difficulty for me is to access the hasBeenRead in the JSON from the view without iterating. Just asking "is there at least one unread message?".
Is there a way to do it in this way?
Create filter as below
app.filter('hasBeenRead', function () {
return function (input) {
return input.some(val => !val.createdBy.hasBeenRead);
}
});
<div ng-if="messages.length > 0">
<button ng-if="!messages|hasBeenRead">Mark all read</button>
<button ng-if="messages|hasBeenRead">Mark all unread</button>
</div>

How to define a temporary variable in Vue.js template

Here is my current template:
<a-droppable v-for="n in curSize" :key="n - 1" :style="{width: `${99.99 / rowLenMap[orderList[n - 1]]}%`, order: orderList[n - 1]}">
<a-draggable :class="{thin: rowLenMap[orderList[n - 1]] > 10}">
<some-inner-element>{{rowLenMap[orderList[n - 1]]}}</some-inner-element>
</a-draggable>
</a-droppable>
The problem is that i have to write rowLenMap[orderList[n - 1]] multiple times, and i'm afraid vue.js engine will also calculate it multiple times.
What i want is something like this:
<a-droppable v-for="n in curSize" :key="n - 1" v-define="rowLenMap[orderList[n - 1]] as rowLen" :style="{width: `${99.99 / rowLen}%`, order: orderList[n - 1]}">
<a-draggable :class="{thin: rowLen > 10}">
<some-inner-element>{{rowLen}}</some-inner-element>
</a-draggable>
</a-droppable>
I think it's not difficult to implement technically because it can be clumsily solved by using something like v-for="rowLen in [rowLenMap[orderList[n - 1]]]". So is there any concise and official solution?
I found a very simple (almost magical) way to achieve that,
All it does is define an inline (local) variable with the value you want to use multiple times:
<li v-for="id in users" :key="id" :set="user = getUser(id)">
<img :src="user.avatar" />
{{ user.name }}
{{ user.homepage }}
</li>
Note : set is not a special prop in Vuejs, it's just used as a placeholder for our variable definition.
Source: https://dev.to/pbastowski/comment/7fc9
CodePen: https://codepen.io/mmghv/pen/dBqGjM
Update : Based on comments from #vir us
This doesn't work with events, for example #click="showUser(user)" will not pass the correct user, rather it will always be the last evaluated user, that's because the user temp variable will get re-used and replaced on every circle of the loop.
So this solution is only perfect for template rendering because if component needs re-render, it will re-evaluate the variable again.
But if you really need to use it with events (although not advisable), you need to define an outer array to hold multiple variables at the same time :
<ul :set="tmpUsers = []">
<li v-for="(id, i) in users" :key="id" :set="tmpUsers[i] = getUser(id)" #click="showUser(tmpUsers[i])">
<img :src="tmpUsers[i].avatar" />
{{ tmpUsers[i].name }}
{{ tmpUsers[i].homepage }}
</li>
</ul>
https://codepen.io/mmghv/pen/zYvbPKv
credits : #vir us
Although it doesn't make sense here to basically duplicate the users array, this could be handy in other situations where you need to call expensive functions to get the data, but I would argue you're better off using computed property to build the array then.
Judging by your template, you're probably best off with a computed property, as suggested in the accepted answer.
However, since the question title is a bit broader (and comes up pretty high on Google for "variables in Vue templates"), I'll try to provide a more generic answer.
Especially if you don't need every item of an array transformed, a computed property can be kind of a waste. A child component may also be overkill, in particular if it's really small (which would make it 20% template, 20% logic and 60% props definition boilerplate).
A pretty straightforward approach I like to use is a small helper component (let's call it <Pass>):
const Pass = {
render() {
return this.$scopedSlots.default(this.$attrs)
}
}
Now we can write your component like this:
<Pass v-for="n in curSize" :key="n - 1" :rowLen="rowLenMap[orderList[n - 1]]" v-slot="{ rowLen }">
<a-droppable :style="{width: `${99.99 / rowLen}%`, order: orderList[n - 1]}">
<a-draggable :class="{thin: rowLen > 10}">
<some-inner-element>{{rowLen}}</some-inner-element>
</a-draggable>
</a-droppable>
</Pass>
<Pass> works by creating a scoped slot. Read more about scoped slots on the Vue.js documentation or about the approach above in the dev.to article I wrote on the topic.
Appendix: Vue 3
Vue 3 has a slightly different approach to slots. First, the <Pass> component source code needs to be adjusted like this:
const Pass = {
render() {
return this.$slots.default(this.$attrs)
}
}
Today I needed this and used <template> tag and v-for like this
I took this code and
<ul>
<li v-for="key in keys"
v-if="complexComputation(key) && complexComputation(key).isAuthorized">
{{complexComputation(key).name}}
</li>
</ul>
Changed it to this
<ul>
<template v-for="key in keys">
<li v-for="complexObject in [complexComputation(key)]"
v-if="complexObject && complexObject.isAuthorized">
{{complexObject.name}}
</li>
</template>
</ul>
And it worked and I was pleasantly surprised because I didn't know this was possible
This seems like the perfect use case of a child component. You can simply pass your complex computed value(s) as a property to the component.
https://v2.vuejs.org/v2/guide/components.html#Passing-Data-to-Child-Components-with-Props
How about this:
<div id="app">
<div
v-for="( id, index, user=getUser(id) ) in users"
:key="id"
>
{{ user.name }}, {{ user.age }} years old
<span #click="show(user)">| Click to Show {{user.name}} |</span>
</div>
</div>
CodePen: https://codepen.io/Vladimir-Miloevi/pen/xxJZKKx
<template>
<div>
<div v-for="item in itemsList" :key="item.id">
{{ item.name }}
<input v-model="item.description" type="text" />
<button type="button" #click="exampleClick(item.id, item.description)">
Click
</button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
items: [
{
id: 1,
name: 'Name1',
},
{
id: 2,
name: 'Name2',
},
],
}
},
computed: {
itemsList() {
return this.items.map((item) => {
return Object.assign(item, { description: '' })
})
},
},
methods: {
exampleClick(id, description) {
alert(JSON.stringify({ id, description }))
},
},
}
</script>
Just tested using vue3 and works, i think it works universally
{{ (somevariable = 'asdf', null) }}
<span v-if="somevariable=='asdf'">Yey</span>
<span v-else>Ney</span>
It outputs nothing while setting your variable.
mandatory:
opening "("
set your variable
closing ", null)"
curSize is an array. Your temporary values comprise a corresponding implied array sizedOrderList = curSize.map(n => orderList[n-1]). If you define that as a computed, your HTML becomes
<a-droppable v-for="n, index in sizedOrderList" :key="curSize[index]" :style="{width: `${99.99 / rowLenMap[n]}%`, order: n}">
<a-draggable :class="{thin: rowLenMap[n] > 10}">
<some-inner-element>{{rowLenMap[n]}}</some-inner-element>
</a-draggable>
</a-droppable>

VueJs: How to Edit an Array Item

Simple Todo-App. Please excuse my ignorance for making a rather basic question.
But how would you go about and edit a certain item on an array?
Normally I would try to bind the value of my input to a new property on my data object and then assign this new property to the old property on click throuch Vue's two way databinding.
Like this: http://jsfiddle.net/z7960up7/
Well in my case I use the v-repeat directive, which loops through my data array but I can't use the v-model directive to use the two way databinding, because the values of the properties get corrupted if I do so. (See here: http://jsfiddle.net/doL46etq/2/)
And now I wonder, how I would go about updating my array of tasks:
My idea is to pass the VueObject (this) through my method on click, and then define the index on my event handler and then updating the tasks array, using the index, like this:
HTML:
<input v-el="editInputField" type="text" value="{{ task.body }}" v-on="keyup: doneEdit(this) | key 'enter'"/>
<button v-on="click: editTask(this)">
Edit
</button>
JS:
methods: {
editTask: function (task) {
var taskIndex = this.tasks.indexOf(task.task);
this.tasks[taskIndex] = {
'body': document.querySelector('input').value,
'completed': false
};
console.log(task.task.body);
},
}
Here is my fiddle about it:
http://jsfiddle.net/doL46etq/3/
But the data object is not updated at all and I wonder how I would go about it and update it.
What is the best way to edit an element on the array, using Vue?
Edit: An easy way, would just be to delete the element, and add the new to the array, using the push method, but I really want just to update the element, because I like to keep the dataobject in sync with my backend.
The short answer: Use a component in an extended constructor, then pass the index to that component in HTML as property and use computed properties to link back and forth to your data.
But don't be satisfied with just the short answer. Here is the long one:
Situation: I am using your JSFiddle as base for this answer.
in HTML you have:
<td>{{ task.body }}</td>
<td>
<div>
<input v-el="editInputField" type="text" value="{{ task.body }}" v-on="keyup: doneEdit(this) | key 'enter'" v-model="newEdit"/>
</div>
</td>
<td>
<button v-on="click: editTask(this)" class="mdl-button mdl-js-button mdl-button--icon"> <i class="material-icons">create</i>
</button>
</td>
We want to replace this code with the component. Using this component allows us to identify the index/row we are working on in your set of data.
<td v-component="listitem" index="{{$index}}"></td>
Next step: defining the component.
In order not to cloud our instance with the component, we will create a separate constructor for the vue object, so we can assign the new element to our new object.
This is done using extend.
window.newVue = Vue.extend({
components:
{
'listitem': {
props: ['index'],
computed: {
// both get and set
body: {
get: function () {
return this.$parent.tasks[this.index].body;
},
set: function (v) {
this.$parent.tasks[this.index].body = v
}
}
},
template: '<td>{{ body }}</td><td><div><input type="text" v-model="body" value="{{ body }}"/></div></td><td></td>',
}
}
});
Since we can't define our data properly using an extend, we'll just assume the data already exists while writing the component.
Step 3: defining the actual data:
Instead of using Vue as our object constructor, we'll now use our newly created instantiator.
new newVue({
el: '#todoapp',
data: {
tasks: [{
'body': 'Eat breakfast',
'completed': false
}, {
'body': 'Drink milk',
'completed': false
}, {
'body': 'Go to the store',
'completed': false
}],
newTask: '',
},
});
That's it!
In case you couldn't follow what happened: Here's the Fiddle!
PS: More information about the working of these code can be found on vuejs.org
Actually the simplest way to update an array item, is to two-way bind the task body with the v-model directive.
Example:
http://jsfiddle.net/z7960up7/2/
<div id="demo">
{{ message }}
<div class="edit">
<input type="text" v-model="message">
<button v-on="click: editMessage">Edit</button>
</div>
<pre>{{ $data | json }}</pre>
</div>
And fire an event whenever you blur out of the input box or the edit button is hit.
Also hide the input field with css, by using the v-class directive.

Categories