I have a SPA where I show array of pokemon using v-for, with the option to filter those lists by type or generation. I have a button that clears the filters (sets the type to '' and generation to generation 1), but the v-for loop doesn't re-render the array after the filters are cleared. I've logged the function that returns the array of pokemon to confirm it's working, but Vue JS doesn't render the results. I'm not sure how to proceed.
<div class="pokemon"
v-for="pokemon in filteredPokemon"
:key="pokemon.id">
<h2>{{ pokemon.name }}</h2>
</div>
<script>
import Pokemon from '../pokeData'
export default{
props: ['searchFilters'],
data(){
return{
allPokemon: [],
}
},
created(){
this.allPokemon = Pokemon.getPokemon('gen1');
},
computed: {
filteredPokemon: function(){
if(this.searchFilters.type){
if(this.searchFilters.type === ''){
return this.allPokemon
}
return this.allPokemon.filter(pokemon => {
if(pokemon.types.length === 2){
if(pokemon.types[0].type.name == this.searchFilters.type || pokemon.types[1].type.name == this.searchFilters.type){
return true
}
}
else if(pokemon.types[0].type.name == this.searchFilters.type){
return true
}
})
}
return this.allPokemon
}
},
watch:{
'searchFilters.generation': function(generation){
this.allPokemon = Pokemon.getPokemon(generation)
}
}
}
}
</script>
farincz is right, you are changing the attributes of allPokemon with the function call to getPokemon and Vue.JS can't find the change (documentation), therefore it's a caveat and you would need to handle this in a different way because Vue doesn't support the way you want it.
I would filter all pokemons with a filter method with a computed value and bind the filter value to a data property:
HTML:
<template>
<div>
<textarea v-model="text" name="filter" cols="30" rows="2"></textarea>
<div class="pokemon" v-for="pokemon in filteredPokemon" :key="pokemon.id">
<h2>{{ pokemon.name }}</h2>
</div>
</div>
</template>
JS file:
new Vue({
el: "#app",
data(){
return{
text: '',
pokemons: [
{gen: 'gen1', name: 'psyduck', id: '1'},
{gen: 'gen1', name: 'charizard', id: '2'},
{gen: 'gen1', name: 'pikachu', id: '3'},
{gen: 'gen2', name: 'togapi', id: '4'}
]
}
},
computed: {
filteredPokemon() {
if(this.text === '') return this.pokemons
return this.pokemons.filter(x=>x.gen === this.text)
}
}
})
here's the jsfiddle to play around.
Related
I'm working with BootstrapVue. I have a b-button in my template where I want to switch its text after clicking on it.
I'm passing my generated unique item.id to my script, but this is not working out in the case that every b-button text will be changed and not only the one I'm clicking. What is the problem in here?
You should be able to copy, paste the code and it should work out.
Please notice that it's just a replica of my code shortened on needed code, so the code should not be changed this much.
My template:
<div v-for="item in inputs" :key="item.id">
<b-button #click="changeText(item)">{{btnText}}</b-button>
</div>
<b-button #click="addInput">Add Input</b-button>
My script:
data() {
return {
collapsed: [true],
id: null,
inputs: [{id: 0}],
btnText: "It's false",
}
},
methods: {
changeText(item) {
this.collapsed[item.id] = !this.collapsed[item.id]
if(this.collapsed[item.id] === true) {
this.btnText = "It's true"
}
else if(this.collapsed[item.id] === false) {
this.btnText = "It's false"
}
},
addInput() {
this.inputs.push({
id: this.id += 1,
})
this.collapsed.push(true);
}
}
Instead of having separate arrays to store the different fields, move them into the inputs[] object array, so that each item in the v-for has its own id, collapsed, and btnText.
Then update changeText() to refer to the fields within the item argument.
Also update the template to use the item.btnText field.
<script>
export default {
data() {
return {
inputs: [{ id: 0, collapsed: true, btnText: `It's true` }], 1️⃣
}
},
methods: {
changeText(item) {
item.collapsed = !item.collapsed 2️⃣
if (item.collapsed) {
item.btnText = `It's true`
} else {
item.btnText = `It's false`
}
},
addInput() {
this.inputs.push({ 1️⃣
id: this.id++,
collapsed: true,
btnText: `It's true`,
})
},
},
}
</script>
<template>
<div>
<div v-for="item in inputs" :key="item.id"> 3️⃣
<b-button #click="changeText(item)">{{ item.btnText }}</b-button>
</div>
<b-button #click="addInput">Add Input</b-button>
</div>
</template>
demo
You can maintain an object for btnText like
data() {
return {
collapsed: [false],
id: null,
inputs: [{id: 0}],
btnText: {0: "It's false"} //Change added
}
},
methods: {
changeText(item) {
this.collapsed[item.id] = !this.collapsed[item.id]
if(this.collapsed[item.id] === true) {
this.btnText[item.id] = "It's true"; //Change added
}
else if(this.collapsed[item.id] === false) {
this.btnText[item.id] = "It's false"; //Change added
}
},
addInput() {
this.inputs.push({
id: this.inputs.length, // Change added
})
this.btnText[this.inputs.length] = 'It's false'; //Change added
this.collapsed.push(true);
}
}
and your template should be like
<div v-for="item in inputs" :key="item.id">
<b-button #click="changeText(item)">{{btnText[item.id]}}</b-button> <!-- Change added -->
</div>
<b-button #click="addInput">Add Input</b-button>
Suppose, My Name is "Aerofoil Todo Kite" I want AK
I got a code from Stackoverflow. I hope it will work. But my question is, I am printing data from Array of Objects with v-for loop.
How do I pass the Name to compute that?
I think, Computed Property don't accept Parameter.
Then what will be the process??
Method can do. But It is calling for many times!!!
data:
tableData: [
{ customer: 'EE Fashion'},
{ customer: 'Tom Hangs Ron'}
}]
methods: {
nameOfCompany(fullName) {
console.log(fullName);
return "HL";
}
}
Code of Mine:
<template slot-scope="scope">
<p style="margin-top: 5px;"><b>{{ nameOfCompany(scope.row.customer) }}</b></p>
</template>
Here is the problem:
{{ nameOfCompany(scope.row.customer) }}
This function is calling for many times!!!!
What will be the approach to do that?
You may write a Customer component so you only compute the company's name once:
It takes a name, and in data, computes the associated companyName.
const mytable = {
props: ['rows'],
template: `
<table>
<tr v-for="row in rows">
<slot :row="row"></slot>
</tr>
</table>
`
}
const mycustomer = {
props: ['name'],
data () {
return {
companyName: this.name.split(' ').map(x => x[0].toUpperCase()).join('')
}
},
template: `
<td>{{ name }} - <abbr>{{ companyName }}</abbr></td>
`
}
let vm = new Vue({
el:'#el',
components: { mytable, mycustomer },
template: `
<mytable :rows="['grod zi', 'tu rok']">
<template v-slot:default="{ row: user }">
<mycustomer :name="user"/>
</template>
</mytable>
`
});
abbr {
color: blue;
}
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="el"></div>
You can use either filters or computed.
Filters: Vue.js allows you to define filters that can be used to apply common text formatting. Filters are usable in two places: mustache
interpolations and v-bind expressions (the latter supported in
2.1.0+). Filters should be appended to the end of the JavaScript expression, denoted by the “pipe” symbol: doc
new Vue({
el: "#app",
data: {
tableData: [
{ customer: 'EE Fashion', company_name: "FOO BAR" },
{ customer: 'Tom Hangs Ron', company_name: "BAZ FOO BAR"},
{ customer: 'Jerry', company_name: "Lorem Ipsum Dorsum Zaren" }
],
},
filters: {
short_hand (company_name) {
// You can put your logic here...
let words = company_name.split(" ")
let short_hand = words[0][0] + words[words.length-1][0]
return short_hand // <-- The return value as Per logic
}
},
computed: {
getTableData () {
return this.tableData.map(data => {
let words = data.company_name.split(" ")
let short_hand = words[0][0] + words[words.length-1][0]
return { short_hand, ...data }
})
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
USING FILTER: <br>
<div
v-for="(data, i) in tableData"
:key="'using-filter-'+i"
>
{{ data.company_name | short_hand }}
</div>
<hr> USING COMPUTED: <br>
<div
v-for="(data, i) in getTableData"
:key="'using-computed-'+i"
>
{{ data.short_hand }}
</div>
</div>
I've created a simple component named DefaultButton.
It bases on properties, that are being set up whenever this component is being created.
The point is that after mounting it, It does not react on changes connected with "defaultbutton", that is an object located in properties
<template>
<button :class="buttonClass" v-if="isActive" #click="$emit('buttonAction', defaultbutton.id)" >
{{ this.defaultbutton.text }}
</button>
<button :class="buttonClass" v-else disabled="disabled">
{{ this.defaultbutton.text }}
</button>
</template>
<script>
export default {
name: "defaultbutton",
props: {
defaultbutton: Object
},
computed: {
buttonClass() {
return `b41ngt ${this.defaultbutton.state}`;
},
isActive() {
return (this.defaultbutton.state === "BUTTON_ACTIVE" || this.defaultbutton.state === "BUTTON_ACTIVE_NOT_CHOSEN");
}
}
};
</script>
Having following component as a parent one:
<template>
<div v-if="state_items.length == 2" class="ui placeholder segment">
{{ this.state_items[0].state }}
{{ this.state_items[1].state }}
{{ this.current_active_state }}
<div class="ui two column very relaxed stackable grid">
<div class="column">
<default-button :defaultbutton="state_items[0]" #buttonAction="changecurrentstate(0)"/>
</div>
<div class="middle aligned column">
<default-button :defaultbutton="state_items[1]" #buttonAction="changecurrentstate(1)"/>
</div>
</div>
<div class="ui vertical divider">
Or
</div>
</div>
</template>
<script type="text/javascript">
import DefaultButton from '../Button/DefaultButton'
export default {
name: 'changestatebox',
data() {
return {
current_active_state: 1
}
},
props: {
state_items: []
},
components: {
DefaultButton
},
methods: {
changecurrentstate: function(index) {
if(this.current_active_state != index) {
this.state_items[this.current_active_state].state = 'BUTTON_ACTIVE_NOT_CHOSEN';
this.state_items[index].state = 'BUTTON_ACTIVE';
this.current_active_state = index;
}
},
},
mounted: function () {
this.state_items[0].state = 'BUTTON_ACTIVE';
this.state_items[1].state = 'BUTTON_ACTIVE_NOT_CHOSEN';
}
}
</script>
It clearly shows, using:
{{ this.state_items[0].state }}
{{ this.state_items[1].state }}
{{ this.current_active_state }}
that the state of these items are being changed, but I am unable to see any results on the generated "DefaultButtons". Classes of objects included in these components are not being changed.
#edit
I've completely changed the way of delivering the data.
Due to this change, I've abandoned the usage of an array; instead I've used two completely not related object.
The result is the same - class of the child component's object is not being
DefaulButton.vue:
<template>
<button :class="buttonClass" v-if="isActive" #click="$emit('buttonAction', defaultbutton.id)" >
{{ this.defaultbutton.text }}
</button>
<button :class="buttonClass" v-else disabled="disabled">
{{ this.defaultbutton.text }}
</button>
</template>
<style lang="scss">
import './DefaultButton.css';
</style>
<script>
export default {
name: "defaultbutton",
props: {
defaultbutton: {
type: Object,
default: () => ({
id: '',
text: '',
state: '',
})
}
},
computed: {
buttonClass() {
return `b41ngt ${this.defaultbutton.state}`;
},
isActive() {
return (this.defaultbutton.state === "BUTTON_ACTIVE" ||
this.defaultbutton.state === "BUTTON_ACTIVE_NOT_CHOSEN");
}
}
};
</script>
ChangeStateBox.vue:
<template>
<div class="ui placeholder segment">
{{ this.state_first.state }}
{{ this.state_second.state }}
{{ this.current_active_state }}
<div class="ui two column very relaxed stackable grid">
<div class="column">
<default-button :defaultbutton="state_first" #buttonAction="changecurrentstate(0)"/>
</div>
<div class="middle aligned column">
<default-button :defaultbutton="state_second" #buttonAction="changecurrentstate(1)"/>
</div>
</div>
<div class="ui vertical divider">
Or
</div>
</div>
</template>
<script type="text/javascript">
import DefaultButton from '../Button/DefaultButton'
export default {
name: 'changestatebox',
data() {
return {
current_active_state: 1
}
},
props: {
state_first: {
type: Object,
default: () => ({
id: '',
text: ''
})
},
state_second: {
type: Object,
default: () => ({
id: '',
text: ''
})
},
},
components: {
DefaultButton
},
methods: {
changecurrentstate: function(index) {
if(this.current_active_state != index) {
if(this.current_active_state == 1){
this.$set(this.state_first, 'state', "BUTTON_ACTIVE_NOT_CHOSEN");
this.$set(this.state_second, 'state', "BUTTON_ACTIVE");
} else {
this.$set(this.state_first, 'state', "BUTTON_ACTIVE");
this.$set(this.state_second, 'state', "BUTTON_ACTIVE_NOT_CHOSEN");
}
this.current_active_state = index;
}
},
},
created: function () {
this.state_first.state = 'BUTTON_ACTIVE';
this.state_second.state = 'BUTTON_ACTIVE_NOT_CHOSEN';
}
}
</script>
You're declaring props wrong. It is either an array of prop names or it is an object with one entry for each prop declaring its type, or it is an object with one entry for each prop declaring multiple properties.
You have
props: {
state_items: []
},
but to supply a default it should be
props: {
state_items: {
type: Array,
default: []
}
},
But your problem is most likely that you're mutating state_items in such a way that Vue can't react to the change
Your main problem is the way you are changing the button state, according with Array change detection vue can't detect mutations by indexing.
Due to limitations in JavaScript, Vue cannot detect the following
changes to an array:
When you directly set an item with the index, e.g.
vm.items[indexOfItem] = newValue When you modify the length of the
array, e.g. vm.items.length = newLength
In case someone will be having the same issue:
#Roy J as well as #DobleL were right.
The reason behind this issue was related with the wrong initialization of state objects.
According to the documentation:
Vue cannot detect property addition or deletion.
Since Vue performs the getter/setter conversion process during instance
initialization, a property must be present in the
data object in order for Vue to convert it and make it reactive.
Before reading this sentence, I used to start with following objects as an initial data:
var local_state_first = {
id: '1',
text: 'Realized',
};
var local_state_second = {
id: '2',
text: 'Active'
};
and the correct version of it looks like this:
var local_state_first = {
id: '1',
text: 'Realized',
state: 'BUTTON_ACTIVE'
};
var local_state_second = {
id: '2',
text: 'Active',
state: 'BUTTON_ACTIVE'
};
whereas declaring the main component as:
<change-state-box :state_first="local_state_first" :state_second="local_state_second" #buttonAction="onbuttonAction"/>
Rest of the code remains the same ( take a look at #edit mark in my main post )
I have a data structure with nested objects that I want to bind to sub-components, and I'd like these components to edit the data structure directly so that I can save it all from one place. The structure is something like
job = {
id: 1,
uuid: 'a-unique-value',
content_blocks: [
{
id: 5,
uuid: 'some-unique-value',
block_type: 'text',
body: { en: { content: 'Hello' }, fr: { content: 'Bonjour' } }
},
{
id: 9,
uuid: 'some-other-unique-value',
block_type: 'text',
body: { en: { content: 'How are you?' }, fr: { content: 'Comment ça va?' } }
},
]
}
So, I instantiate my sub-components like this
<div v-for="block in job.content_blocks" :key="block.uuid">
<component :data="block" :is="contentTypeToComponentName(block.block_type)" />
</div>
(contentTypeToComponentName goes from text to TextContentBlock, which is the name of the component)
The TextContentBlock goes like this
export default {
props: {
data: {
type: Object,
required: true
}
},
created: function() {
if (!this.data.body) {
this.data.body = {
it: { content: "" },
en: { content: "" }
}
}
}
}
The created() function takes care of adding missing, block-specific data that are unknown to the component adding new content_blocks, for when I want to dynamically add blocks via a special button, which goes like this
addBlock: function(block_type) {
this.job.content_blocks = [...this.job.content_blocks, {
block_type: block_type,
uuid: magic_uuidv4_generator(),
order: this.job.content_blocks.length === 0 ? 1 : _.last(this.job.content_blocks).order + 1
}]
}
The template for TextContentBlock is
<b-tab v-for="l in ['fr', 'en']" :key="`${data.uuid}-${l}`">
<template slot="title">
{{ l.toUpperCase() }} <span class="missing" v-show="!data.body[l] || data.body[l] == ''">(missing)</span>
</template>
<b-form-textarea v-model="data.body[l].content" rows="6" />
<div class="small mt-3">
<code>{{ { block_type: data.block_type, uuid: data.uuid, order: data.order } }}</code>
</div>
</b-tab>
Now, when I load data from the API, I can correctly edit and save the content of these blocks -- which is weird considering that props are supposed to be immutable.
However, when I add new blocks, the textarea above wouldn't let me edit anything. I type stuff into it, and it just deletes it (or, I think, it replaces it with the "previous", or "initial" value). This does not happen when pulling content from the API (say, on page load).
Anyway, this led me to the discovery of immutability, I then created a local copy of the data prop like this
data: function() {
return {
block_data: this.data
}
}
and adjusted every data to be block_data but I get the same behaviour as before.
What exactly am I missing?
As the OP's comments, the root cause should be how to sync textarea value between child and parent component.
The issue the OP met should be caused by parent component always pass same value to the textarea inside the child component, that causes even type in something in the textarea, it still bind the same value which passed from parent component)
As Vue Guide said:
v-model is essentially syntax sugar for updating data on user input
events, plus special care for some edge cases.
The syntax sugar will be like:
the directive=v-model will bind value, then listen input event to make change like v-bind:value="val" v-on:input="val = $event.target.value"
So adjust your codes to like below demo:
for input, textarea HTMLElement, uses v-bind instead of v-model
then uses $emit to popup input event to parent component
In parent component, uses v-model to sync the latest value.
Vue.config.productionTip = false
Vue.component('child', {
template: `<div class="child">
<label>{{value.name}}</label><button #click="changeLabel()">Label +1</button>
<textarea :value="value.body" #input="changeInput($event)"></textarea>
</div>`,
props: ['value'],
methods: {
changeInput: function (ev) {
let newData = Object.assign({}, this.value)
newData.body = ev.target.value
this.$emit('input', newData) //emit whole prop=value object, you can only emit value.body or else based on your design.
// you can comment out `newData.body = ev.target.value`, then you will see the result will be same as the issue you met.
},
changeLabel: function () {
let newData = Object.assign({}, this.value)
newData.name += ' 1'
this.$emit('input', newData)
}
}
});
var vm = new Vue({
el: '#app',
data: () => ({
options: [
{id: 0, name: 'Apple', body: 'Puss in Boots'},
{id: 1, name: 'Banana', body: ''}
]
}),
})
.child {
border: 1px solid green;
}
<script src="https://unpkg.com/vue#2.5.16/dist/vue.js"></script>
<div id="app">
<span> Current: {{options}}</span>
<hr>
<div v-for="(item, index) in options" :key="index">
<child v-model="options[index]"></child>
</div>
</div>
Well I hope to explain, I'm generating this data from a component, when I click the checkbox changes in the generated data are not reflected, but when clicking the button with a data already in the instance changes are made, I appreciate if you explain Why or do they have a solution?
this my code
js
Vue.component('fighters', {
template: '#fighters-template',
data() {
return {
items: [
{ name: 'Ryu' },
{ name: 'Ken' },
{ name: 'Akuma' }
],
msg: 'hi'
}
},
methods: {
newData() {
this.items.forEach(item => {
item.id = Math.random().toString(36).substr(2, 9);
item.game = 'Street Figther';
item.show = false;
});
this.items.push()
},
greeting() {
this.msg = 'hola'
}
}
});
new Vue({
el: '#app'
})
html
<main id="app">
<fighters></fighters>
</main>
<template id="fighters-template">
<div class="o-container--sm u-my1">
<ul>
<li v-for="item in items">
<input type="checkbox" v-model="item.show">
{{ item.name }}</li>
</ul>
<button class="c-button c-button--primary" #click="newData()">New Data</button>
<h2>{{ msg }}</h2>
<button class="c-button c-button--primary" #click="greeting()">Greeting</button>
<hr>
<pre>{{ items }}</pre>
</div>
</template>
this live code
https://jsfiddle.net/cqx12a00/1/
Thanks for you help
You don't declare the show variables that your checkboxes are bound to, so they are not reactive – Vue is not aware when one is updated.
It should be initialized like so:
items: [
{ name: 'Ryu', show: false },
{ name: 'Ken', show: false },
{ name: 'Akuma', show: false }
]
You might think that newData would fix it, since it assigns a show member to each item, but Vue cannot detect added properties so they're still not reactive. If you initialized the data as I show above, then the assignment would be reactive.
If you want to add a new reactive property to an object, you should use Vue.set.