I have JSON
"8":{
"name": "Dog",
"age": 2,
"showModal": false
},
"9":{
"name": "Cat",
"age": 7,
"showModal": false
},
Next I used v-for to show data:
<div v-for="(animal, index) in animals" :key="index">
<div>{{ animal.name }}</div> // Dog
<div>{{ animal.showModal }}</div> // false
<div #click="showModal(animal.showModal)">Show modal</div>
<div v-show="animal.showModal">modal...</div>
</div>
Method function:
showModal(showModal){
showModal = !showModal;
}
But function showModal not change value from false to true. How I can change this value to show modal ?
If you are loading from file you can try like this:
<div v-for="(animal, index) in animals" :key="index">
<div>{{ animal.name }}</div> // Dog
<div>{{ animal.showModal }}</div> // false
<div #click="showModal(index)">Show modal</div>
<div v-if="isItemClicked(index)">modal...</div>
</div>
In your data initialize indexes: [] and go and see if your index in that array.
methods: {
showModal(index) {
this.indexes.push(index);
},
isItemClicked(index) {
let test = false;
this.indexes.forEach(i => {
if (i === index) {
test = true;
break;
}
})
return test;
}
}
If you need to change data in your json you will need other procedure.
I think it was helpful. Good luck.
You have a typo in #clikc. Should be #click.
Tempalte:
<div #click="showModal(animal)">Show modal</div>.
Method:
showModal(animal) {
animal.showModal = true;
}
Related
I am trying to use VueJS 2 to render a simple v-for, where I would like the loop variable to represent the property I would like to access. This is a minimal example showing what I would like to achieve:
var app = new Vue({
el: '#app',
data() {
return {
properties: ["a", "b", "c"],
a: "Value Of A",
b: "Value Of B",
c: "Value Of C"
};
}
})
<div id="app">
<div> this renders fine: a = {{ this['a'] }} </div>
<div>
but this doesn't:
<div v-for="name in properties" :key="name"> {{name}} = {{ this[name] }}</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
Basically, I would like to render data fields selectively, and in the specific ordering dictated by the properties field. But the example cannot render anything for {{ this[name] }}. In fact it does not even render if I changed it to {{ this['a'] }} for a quick testing. {{ this['a'] }} if put outside the v-for block will render fine.
What could be the issue for the v-for block?
Everything in a vue template has this automatic, by convention. In fact, you cannot access anything other than properties, data, methods, computed, filters, etc outside of the this component instance in the template.
So this is not available because everything you declare in the template implies it.
After digging into the compiled template (via Vue Template Explorer), I came to realize that this (simplified to just contain the problematic v-for) template:
<div id="app">
<div v-for="name in properties" :key="name"> {{name}} = {{ this[name] }}</div>
</div>
is compiled to:
function render() {
with(this) {
return _c('div', {
attrs: {
"id": "app"
}
}, _l((properties), function (name) {
return _c('div', {
key: name
}, [_v(" " + _s(name) + " = " + _s(this[name]))])
}), 0)
}
}
where _c is short for createElement, _l is short for renderList, and _v is short for createTextVNode
Now it is clear that the reason I can write {{ a }} without mentioning where to resolve a is because of the with(this) expression where this being the proxy object.
However, the problem with v-for is that the actual item renderer is passed as a callback function into _l:
function (name) {
return _c('div',
{key: name},
[_v(" " + _s(name) + " = " + _s(this[name]))])
}
The way the callback will be called, the default binding rule applies and this is bound to the global window object.
With VueJS 3 (template explorer), the same template is compiled a little different:
import { renderList as _renderList, Fragment as _Fragment, openBlock as _openBlock, createBlock as _createBlock, toDisplayString as _toDisplayString, createVNode as _createVNode } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", { id: "app" }, [
(_openBlock(true), _createBlock(_Fragment, null, _renderList(_ctx.properties, (name) => {
return (_openBlock(), _createBlock("div", { key: name }, _toDisplayString(name) + " = " + _toDisplayString(this[name]), 1 /* TEXT */))
}), 128 /* KEYED_FRAGMENT */))
]))
}
Notice that the list render function is now defined as a lambda expression, hence this is lexically captured.
So to get the whole thing to work, I ended up with the method others suggested in comments:
let app = new Vue({
el: '#app',
data() {
return {
properties: ["a", "b", "c", "d"],
a: "Value Of A",
b: "Value Of B",
c: "Value Of C"
};
},
computed: {
d() {
return "Value of D";
}
},
methods: {
get(prop) {
return this[prop];
}
},
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div>
from $data:
<div v-for="name in properties" :key="name"> {{name}} = {{ $data[name] }}</div>
</div>
<div>
from get:
<div v-for="name in properties" :key="name"> {{name}} = {{ get(name) }}</div>
</div>
</div>
What do I have: components structure
<Games> // using v-for - iterate all games
--<Game> // using v-for - iterate all players
----<Player 1>
------<DeleteWithConfirmation>
----<Player 2>
------<DeleteWithConfirmation>
----<Player 3>
------<DeleteWithConfirmation>
----...
<DeleteWithConfirmation> implementation: two clicks are required for deleting game property.
<template>
<div>
<button #click="incrementDelete"
v-html="deleteButtonHTML"></button>
<button v-if="deleteCounter === 1" #click="stopDeleting">
<i class="undo icon"></i>
</button>
</div>
</template>
<script>
export default {
name: 'DeleteWithConfirmation',
data() {
return {
deleteCounter: 0
}
},
computed: {
deleteButtonHTML: function () {
if (this.deleteCounter === 0)
return '<i class="trash icon"></i>'
else
return 'Are you sure?'
}
},
methods: {
incrementDelete() {
this.deleteCounter++
if (this.deleteCounter === 2) {
//tell parent component that deleting is confirmed.
//Parent call AJAX than.
this.$emit('deletingConfirmed')
this.stopDeleting()
}
else if (this.deleteCounter > 2)
this.stopDeleting()
},
stopDeleting() {
Object.assign(this.$data, this.$options.data())
}
}
}
</script>
My problem: seems like indicies are mixed up:
Before deleting 4th player was on "Are you sure state" (deleteCounter === 1), but after deleting it went to initial state (deleteCounter === 0). Seems like 3rd component state haven't updated its deleteCounter, but its data (player's name was updated anyway).
After successfull deleting <Games> component data is fetched again.
You don't need a delete counter for achieving this. On the contrary, it makes it hard to understand your code. Just use a boolean like this:
<template>
<div>
<button #click="clickButton"
<template v-if="confirmation">
<i class="trash icon"></i>
</template>
<template v-else>
Are you sure?
</template>
</button>
<button v-if="confirmation" #click="confirmation = false">
<i class="undo icon"></i>
</button>
</div>
</template>
<script>
export default {
name: 'DeleteWithConfirmation',
data() {
return {
confirmation: false
}
},
methods: {
clickButton() {
if (!this.confirmation) {
this.confirmation = true;
} else {
this.$emit('deleting-confirmed');
}
}
}
</script>
The parent could then be looking e.g. like this:
<div class="row" v-if="showButton">
[...]
<delete-with-confirmation #deleting-confirmed="showButton = false">
</div>
One of the answers was deleted, I wish I could mention the initial author, but I don't remeber his username, so (changed a bit):
incrementDelete() {
if (this.deleteCounter === 1) { // 1 because there is "post-increment" at the end of the fucntion
this.deletingProgress = true
this.$emit('deletingConfirmed')
this.stopDeleting()
}
else if (this.deleteCounter > 1) // 1 because there is "post-increment" at the end of the fucntion
this.stopDeleting()
this.deleteCounter++ // "post-increment"
},
ssc-hrep3 answer is more clean (and lightweight) than mine approach, link.
I have an object with this structure
object: {
"prop1": [],
"prop2": [],
"prop3": [],
}
In my template I want to loop over it and display data in prop's but if there is no data in any of them I want to show something like this
<div>No data</div>
but only once and not for each prop
So far I have this
<div v-for="(props, index) in object" :key="index">
<div v-if="!props.length">
No data
</div>
</div>
But it shows message 3 times, for each prop.
I'm not sure how to solve it. Any help will be much appreciated.
To solve this in a nice way, you should use a computed property.
A computed property is basically a piece of code that give meaningless caclulations meaningful names.
export default {
data() {
return {
object: {
"prop1": [],
"prop2": [],
"prop3": [],
},
};
},
computed: {
areAllEmpty() {
return Object.values(this.object).map(e => e.length).reduce((a, b) => a + b, 0) === 0;
},
}
};
This can then be used in your template as the following:
<template>
<template v-if="areAllEmpty">
<div>No data</div>
</template>
<template v-else>
<div v-for="(props, index) in object" :key="index">
I'm index {{ index }} with length {{ props.length }}
</div>
</template>
</template>
Why v-model does not work with a filter getUppercase in <input v-model="filterText | getUppercase">
HTML
<template>
<div class="wrapper">
Check if fruit exist: <input v-model="filterText | getUppercase">
<ul v-for="fruit in filteredFruits">
<li> {{ fruit }} </li>
</ul>
</div>
</template>
VueJS
export default {
name: "filterText",
data() {
return {
msg: "Welcome to Your Vue.js App",
filterText: "",
fruits: ["Apple", "Banana", "Orange", "PineApple", 'Pina Colada']
};
},
computed: {
filteredFruits: function() {
var vm = this;
return vm.fruits.filter(function(item) {
return item.match(vm.filterText)
});
}
},
filters: {
getUppercase: function(obj) {
return this.obj.toUpperCase();
}
}
};
I can see what you are trying to do, however, because of the two way binding when using v-model, it will be better to just apply the getUppercase filter when displaying.
Your template would be something like this:
<template>
<div class="wrapper">
Check if fruit exist: <input v-model="filterText">
<ul v-for="fruit in filteredFruits">
<li> {{ fruit | getUppercase}} </li>
</ul>
</div>
</template>
But if you still wish to transform the filterText model value, you can use a directive. In that case, your VueJS code will be something like :
Vue.directive('getUppercase', {
twoWay: true, // this transformation applies back to the filterText
bind: function () {
var self = this;
self.handler = function () {
self.set(self.el.value.toUpperCase());
}
self.el.addEventListener('input', self.handler);
},
unbind: function () {
this.el.removeEventListener('input', this.handler);
}
});
Now use this directive in your template like :
<input v-model="filterText" v-get-uppercase="filterText">
It will do the same thing as <input v-model="filterText | getUppercase">
Two ways filters are replaced in vue.js please read the docs for more information.It is good to know.
However,as i understood you want to implement a search in array.See it in action here, or take a look below
<div id="app">
Check if fruit exist: <input v-model="filterText">
<ul v-for="fruit in filteredFruits">
<li> {{ fruit }} </li>
</ul>
</div>
new Vue({
el: "#app",
data: {
filterText: "",
fruits: ["Apple", "Banana", "Orange", "PineApple", 'Pina Colada']
},
computed: {
filteredFruits() {
return this.fruits.filter(item => item.toLowerCase().match(this.filterText.toLowerCase()))
}
}
})
I have a little issue with setting element's width by using v-bind:style=...
The deal is that properties for style are requried faster, than I can provide them (in mounted). Any idea how to force update after I will fill my array with width's?
<template>
<div>
<div class="headings ">
<div class="t-cell head" v-for="(header, index) in headings"
:style="'min-width:'+ getHeight(index) +'px'"
>
{{header}}
</div>
</div>
</div>
<div class="fixed-table text-inline" >
<div class="t-cell head" v-for="(header, index) in headings" :ref="'head' + index">
{{header}}
</div>
</div>
</div>
</template>
<script>
export default {
mounted: function(){
this.getColumnWidths();
},
methods: {
getHeight(index){
return this.headerWidths[index];
},
getColumnWidths(){
const _that=this;
this.headings.forEach(function(element,index){
_that.headerWidths[index] = _that.$refs['head'+index][0].clientWidth
});
},
},
data: function () {
return {
headings: this.headersProp,
headerWidths:[],
}
}
}
</script>
It would be great if there would be some method to enforce update, as the width will probably change based on the content inserted.
Fiddle
https://jsfiddle.net/ydLzucbf/
You are being bitten by the array caveats. Instead of assigning individual array elements (using =), use vm.$set:
getColumnWidths() {
const _that = this;
this.header.forEach(function(element, index) {
_that.$set(_that.headerWidths, index, _that.$refs['head' + index][0].clientWidth)
});
console.log(this.headerWidths);
},