Vue computed value as data property value - vue.js

Simplified example component code:
<template>
<section>
<div>{{ z }}</div>
<div>{{ compZ }}</div>
<div>{{ x }}</div>
</section>
</template>
<script>
export default {
name: "example",
data: () => ({
z: false,
x: [{ visible: null }]
}),
mounted() {
this.x[0].visible = this.compZ;
setTimeout(() => (this.z = true), 1e3);
},
computed: {
compZ() {
return this.z;
}
}
};
</script>
After a second results are:
true
true
[ { "visible": false } ]
I need x[n].visible to change when compZ changes. Any ideas on how to cleanly keep reactivity?
This is required, because i have 22 potential steps, that are visible depending on certain flags that can change after initialization.

You can add watcher for your z.
watch: {
z: function (newValue, oldValue) {
// here you can change x.y
}
},

Found a workaround, but i think it's ugly:
<template>
<section>
<div>{{ refFlag1 }}</div>
<div>{{ compRefFlag1 }}</div>
<div>{{ x }}</div>
</section>
</template>
<script>
export default {
name: "example",
data: () => ({
refFlag1: false,
refFlag2: false,
x: [{ visible: null, visibleFunc: "that.compRefFlag1" }]
}),
watch: {
allRelevatFlags: function () {
setTimeout(() => this.updateVisible());
}
},
mounted() {
this.x[0].visible = this.compRefFlag1;
setTimeout(() => (this.refFlag1 = true), 1e3);
},
methods: {
updateVisible() {
// eslint-disable-next-line no-unused-vars
let that = this; // eval doesn't see 'this' scope
this.x.forEach(step => (step.visible = eval(step.visibleFunc)));
}
},
computed: {
allRelevatFlags() {
return `${this.compRefFlag1}${this.compRefFlag2}`;
},
compRefFlag1() {
return this.refFlag1;
},
compRefFlag2() {
return this.refFlag2;
}
}
};
</script>
Watch for changes in any relevant flag and then using JS eval() set the visible flag anew.
There's got to be a better way...

Related

Props arguments are not reactive in setup

I'm trying to develop a component that compiles the given html; I succeeded with constant html texts, but I can't make this work with changing html texts.
main.js
app.component("dyno-html", {
props: ["htmlTxt"],
setup(props) {
watchEffect(() => {
console.log(`htmlText is: ` + props.htmlTxt);
return compile(props.htmlTxt);
});
return compile(props.htmlTxt);
},
});
Home.js
<template>
<div class="home">
<dyno-html
:htmlTxt="html2"
:bound="myBoundVar"
#buttonclick="onClick"
></dyno-html>
-------------
<dyno-html
:htmlTxt="html"
:bound="myBoundVar"
#buttonclick="onClick"
></dyno-html>
</div>
</template>
<script>
export default {
name: "Home",
components: {},
data: function() {
return {
html: "",
html2: `<div> Static! <button #click="$emit('buttonclick', $event)">CLICK ME</button></div>`
};
},
mounted() {
// get the html from somewhere...
setTimeout(() => {
this.html = `
<div>
Dynamic!
<button #click="$emit('buttonclick', $event)">CLICK ME</button>
</div>
`;
}, 1000);
},
methods: {
onClick(ev) {
console.log(ev);
console.log("You clicked me!");
this.html2 = "<b>Bye Bye!</b>";
},
},
};
</script>
Outcome:
Console:
It seems the changes of htmlText arrives to setup function, but it doesn't affect the compile function!
This is the expected behaviour because prop value is read once and results in static render function.
Prop value should be read inside render function. It can be wrapped with a computed to avoid unneeded compiler calls:
const textCompRef = computed(() => ({ render: compile(props.htmlTxt) }));
return () => h(textCompRef.value);

Render named scopedSlot programmatically

I want to move the following template into the render function of my component, but I don't understand how.
This is my template:
<template>
<div>
<slot name="item" v-for="item in filteredItems" :item="item">
{{ item.title }}
</slot>
</div>
</template>
This is my component:
export default {
props: {
items: {
type: Array,
required: true,
},
search: {
type: String,
default: ""
}
},
methods: {
filterByTitle(item) {
if (!("title" in item)) { return false; }
return item.title.includes(this.search);
}
},
computed: {
filteredItems() {
if (this.search.length === "") {
return this.items;
}
return this.items.filter(this.filterByTitle);
}
},
render: function(h) {
// How can I transform the template so that it finds its place here?
return h('div', ...);
}
};
I thank you in advance.
To render scoped slots you can use $scopedSlots. See more here.
Example Code:
...
render(h) {
return h(
'div',
this.filteredItems.map(item => {
let slot = this.$scopedSlots[item.title]
return slot ? slot(item) : item.title
})
)
}
...
JSFiddle

Vue variable getting undefined inside component

My variable is getting undefined inside component, can anyone Helps me?
The variable is: "professor.nome"
Basically I load my "professor" variable inside the carregarProfessores() method.
Is that a way to load the Titulo component after everything?
This is the component thais is not loading the variable:
<Titulo
:texto="
professorId !== undefined
? 'Professor: ' + professor.nome
: 'Todos os alunos'
"
/>
If I try to access the var like this, works:
<h1>{{ professor.nome }}</h1>
This is my Vue code:
export default {
components: {
Titulo,
},
data() {
return {
professorId: this.$route.params.prof_id,
nome: "",
alunos: [],
professor: [],
};
},
created() {
if (this.professorId) {
this.carregarProfessores();
this.$http
.get("http://localhost:3000/alunos?professor.id=" + this.professorId)
.then((res) => res.json())
.then((alunos) => (this.alunos = alunos));
} else {
this.$http
.get("http://localhost:3000/alunos")
.then((res) => res.json())
.then((alunos) => (this.alunos = alunos));
}
},
props: {},
methods: {
carregarProfessores() {
this.$http
.get("http://localhost:3000/professores/" + this.professorId)
.then((res) => res.json())
.then((professor) => {
this.professor = professor;
});
},
},
};
Here is the Titulo component:
<template>
<div>
<h1>{{ titulo }}</h1>
</div>
</template>
<script>
export default {
props: {
texto: String,
},
data() {
return {
titulo: this.texto,
};
},
};
</script>
The problem is that your Titulo component is stateful. It takes a copy of the initial value of the prop texto but doesn't update it when it changes.
There's no need to take a copy in the first place, just use the prop itself in the template:
<template>
<div>
<h1>{{ texto }}</h1>
</div>
</template>
<script>
export default {
props: {
texto: String
}
};
</script>
Try
data() {
return {
professorId: this.$route.params.prof_id || null, // changed
nome: "",
alunos: [],
professor: null, // changed
};
},
Then
<Titulo
:texto="
professorId && professor
? 'Professor: ' + professor.nome
: 'Todos os alunos'
"
/>
As per your data.
professor is array so you can not access nome direct.
So you have iterator over professor array or
<h1>{{ professor[0].nome }}</h1>

Updating a prop inside a child component so it updates on the parent container too

So I have a simple template like so:
<resume-index>
<div v-for="resume in resumes">
<resume-update inline-template :resume.sync="resume" v-cloak>
//...my forms etc
<resume-update>
</div>
<resume-index>
Now, inside the resume-updatecomponent I am trying to update the prop on the inside so on the outside it doesn't get overwritten, my code is like so;
import Multiselect from "vue-multiselect";
import __ from 'lodash';
export default {
name: 'resume-update',
props: ['resume'],
components: {
Multiselect
},
data: () => ({
form: {
name: '',
level: '',
salary: '',
experience: '',
education: [],
employment: []
},
submitted: {
form: false,
destroy: false,
restore: false
},
errors: []
}),
methods: {
update(e) {
this.submitted.form = true;
axios.put(e.target.action, this.form).then(response => {
this.resume = response.data.data
this.submitted.form = false;
}).catch(error => {
if (error.response) {
this.errors = error.response.data.errors;
}
this.submitted.form = false;
});
},
destroy() {
this.submitted.destroy = true;
axios.delete(this.resume.routes.destroy).then(response => {
this.resume = response.data.data;
this.submitted.destroy = false;
}).catch(error => {
this.submitted.destroy = false;
})
},
restore() {
this.submitted.restore = true;
axios.post(this.resume.routes.restore).then(response => {
this.resume = response.data.data;
this.submitted.restore = false;
}).catch(error => {
this.submitted.restore = false;
})
},
reset() {
for (const prop of Object.getOwnPropertyNames(this.form)) {
delete this.form[prop];
}
}
},
watch: {
resume: function() {
this.form = this.resume;
},
},
created() {
this.form = __.cloneDeep(this.resume);
}
}
When I submit the form and update the this.resume I get the following:
[Vue warn]: Avoid mutating a prop directly since the value will be
overwritten whenever the parent component re-renders. Instead, use a
data or computed property based on the prop's value. Prop being
mutated: "resume"
I have tried adding computed to my file, but that didn't seem to work:
computed: {
resume: function() {
return this.resume
}
}
So, how can I go about updating the prop?
One solution:
simulate v-model
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 the steps:
create one prop = value which you'd like to sync to parent component
inside the child component, create one data porperty=internalValue, then uses Watcher to sync latest prop=value to data property=intervalValue
if intervalValue change, emit one input event to notice parent component
Below is one simple demo:
Vue.config.productionTip = false
Vue.component('container', {
template: `<div>
<p><button #click="changeData()">{{value}}</button></p>
</div>`,
data() {
return {
internalValue: ''
}
},
props: ['value'],
mounted: function () {
this.internalValue = this.value
},
watch: {
value: function (newVal) {
this.internalValue = newVal
}
},
methods: {
changeData: function () {
this.internalValue += '#'
this.$emit('input', this.internalValue)
}
}
})
new Vue({
el: '#app',
data () {
return {
items: ['a', 'b', 'c']
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<div>
<p>{{items}}
<container v-for="(item, index) in items" :key="index" v-model="items[index]">
</container>
</div>
</div>
or use other prop name instead of value (below demo use prop name=item):
Also you can use other event name instead of event name=input.
other steps are similar, but you have to $on the event then implement you own handler like below demo.
Vue.config.productionTip = false
Vue.component('container', {
template: `<div>
<p><button #click="changeData()">{{item}}</button></p>
</div>`,
data() {
return {
internalValue: ''
}
},
props: ['item'],
mounted: function () {
this.internalValue = this.item
},
watch: {
item: function (newVal) {
this.internalValue = newVal
}
},
methods: {
changeData: function () {
this.internalValue += '#'
this.$emit('input', this.internalValue)
this.$emit('test-input', this.internalValue)
}
}
})
new Vue({
el: '#app',
data () {
return {
items: ['a', 'b', 'c']
}
},
methods: {
syncChanged: function (target, index, newData) {
this.$set(target, index, newData)
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<div>
Event Name=input
<p>{{items}}</p>
<container v-for="(item, index) in items" :key="index" :item="item" #input="syncChanged(items, index,$event)">
</container>
</div>
<hr> Event Name=test-input
<container v-for="(item, index) in items" :key="index" :item="item" #test-input="syncChanged(items, index,$event)">
</container>
</div>
I usually use vuex to manage variables that I will be using in multiple components and like the error says, load them in the various components using the computed properties. Then use the mutations property of the store object to handle changes
In component files
computed: {
newProfile: {
get() {
return this.$store.state.newProfile;
},
set(value) {
this.$store.commit('updateNewProfile', value);
}
},
In the vuex store
state: {
newProfile: {
Name: '',
Website: '',
LoginId: -1,
AccountId: ''
}
},
mutations: {
updateNewProfile(state, profile) {
state.newProfile = profile;
}
}

Make Chartkick wait for data to populate from Firebase before rendering chart

I'm using chartkick in my Vue project. Right now, the data is loading from Firebase after the chart has rendered, so the chart is blank. When I change the code in my editor, the chart renders as expected, since it's already been retrieved from Firebase. Is there a way to make chartkick wait for the data to load before trying to render the chart? Thanks!
Line-Chart Component:
<template>
<div v-if="loaded">
<line-chart :data="chartData"></line-chart>
</div>
</template>
<script>
export default {
name: 'VueChartKick',
props: ['avgStats'],
data () {
return {
loaded: false,
chartData: this.avgStats
}
},
mounted () {
this.loaded = true
}
}
</script>
Parent:
<template>
...
<stats-chart v-if="avgStatsLoaded" v-bind:avgStats="avgStats" class="stat-chart"></stats-chart>
<div v-if="!avgStatsLoaded">Loading...</div>
...
</template>
<script>
import StatsChart from './StatsChart'
export default {
name: 'BBall',
props: ['stats'],
components: {
statsChart: StatsChart
},
data () {
return {
avgStatsLoaded: false,
avgStats: []
}
},
computed: {
sortedStats: function () {
return this.stats.slice().sort((a, b) => new Date(b.date) - new Date(a.date))
}
},
methods: {
getAvgStats: function () {
this.avgStats = this.stats.map(stat => [stat.date, stat.of10])
this.avgStatsLoaded = true
}
},
mounted () {
this.getAvgStats()
}
}
modify your code of StatsChart component:
you may use props directly
<template>
<div v-if="loaded">
<line-chart :data="avgStats"></line-chart>
</div>
</template>
export default {
name: 'VueChartKick',
props: ['avgStats'],
data () {
return {
loaded: false,
}
},
mounted () {
this.loaded = true
}
}