Our team is facing a problem with too much logics in template file which causes colleagues hard to debug.
I am thinking a proper way to increase the readability of the template file without losing performance.
Our team often include inline bitwise logic for dynamic class, style, etc. to fulfill the business logic under component template.
[Inline bitwise example]
<template> <!--Inline bitwise way-->
<div class="listContainer">
<div :class="`listItem ${_item.type == '1' ? 'itemTypeOne' : ''} ${_item.type == '2' ? 'itemTypeTwo' : ''}`" v-for="_item in list" :key="_item.key">
<div class="itemBg" :style="{top: _item.type == '1' ? '10px' : '20px'}"></div>
</div>
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
list: [{key: "1", type: "1", value: "Value 1"}, { key: "2", type: "2", value: "Value 2"}],
};
},
methods: {},
computed: {},
}
</script>
To reduce these kind of logic code, I thought of using computed approach but I think it would cause computation overhead (Just in my opinion, feel free to correct me if I was wrong:)). It is because we cannot avoid using parameters with computed which lose the advantage of cache handled by vue itself. By using computed property with parameter approach, the parametrize anonymous function inside computed is keep being called which would potentially cause lower performance.
[Parametrized computed example]
<template>
<div class="listContainer">
<div :class="`listItem ${getItemClass(_item.type)}`" v-for="_item in list" :key="_item.key">
<div class="itemBg" :style="getItemBgStyle(_item.type)"></div>
</div>
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
list: [{key: "1", type: "1", value: "Value 1"}, { key: "2", type: "2", value: "Value 2"}],
};
},
methods: {},
computed: {
getItemClass: function(){
return function($itemType){
console.log("getItemClass called"); /* Keep being called even _item.key didnt changed */
if($itemType == '1'){
return 'itemTypeOne'
}
if($itemType == '2'){
return 'itemTypeTwo'
}
}
},
getItemBgStyle: function(){
return function($itemType){
console.log("getItemClass called"); /* Same as getItemClass */
return {
top: $itemType == '1' ? '10px' : '20px'
}
}
}
},
}
</script>
Same with parametrized computed approach, parametrized method approach also cause this drawback. To keep this question in short, I'm not showing method approach here since it basically same with the computed approach. Here is the reference post that I studied: Can I pass parameters in computed properties in Vue.Js
My question is, is there a proper way to meet my aim (to keep template shorts without losing performance)?
Additionally, could anyone share with us the performance comparison between [Inline bitwise approach] vs [Parametrized computed approach] vs [Parametrized method approach].
Related
I have a tiny snippet that works correctly when I use :src="labels[index]", but my code is growing and I will need to use ${app.labels[index]}. I can do just ${app.labels}, but once I add the [index] part, it breaks. Can someone tell me how to add that inside the brackets?
new Vue({
el: "#app",
data: {
images: [],
labels: [],
form: {
dir: "",
fileItems: [] // An array containing objects of type: {id: number, file: File, dataUrl: [base64 encoded file string], caption: string'}
},
},
methods: {
toggle: function(todo){
todo.done = !todo.done
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<h2>Todos:</h2>
<div class="image-wrap"><img style="max-height: 430px;" :src="${app.labels[index]}" /></div>
</div>
In Vue when you use : in a prop you are basically telling Vue
Please, interpret this expression side as javascript
An example:
<super-aweosme-component :a="1 + 1" />
Will result in the prop a being 2 (because it was interpreted as javasript).
When you don't use : in a component prop you are telling Vue
Please, interpret this as plain text
An example
<super-aweosme-component a="1 + 1" />
Will result in the prop a being literally 1 + 1 (string). So you can change :src="${images.labels[index]}" to just :src="images.labels[index]" (of course, apply the changes mentioned above)
Adition
You have app.labels[index]. app doesn't exist in the template context. You have in that context images, labels and form (of course, there are attributes in that context but not important for this part)
I'm working on a vue cli project where items have two state equipped and unequipped.
This State is controlled by a Boolean located in the Props. Since you can switch the state I had to create a data isEquipped set to false by default.
I then added a watcher but it doesn't change my data value if my props is set to True.
Here's the code
name: "Item",
props: {
Index : Number,
name: String,
desc : String,
bonus: Array,
equipped : Boolean
},
data() {
return {
isEquipped : false
}
},
watch: {
equipped: function(stateEquipped) {
this.isEquipped = stateEquipped;
},
},
So for instance let's say I created a new item with equipped set to True, the watcher doesn't trigger and isEquipped stays at False, is there any reason to that ?
I came across multiple similar questions like this one Vue #Watch not triggering on a boolean change but none of them helped me
If you want to use watch then you can try define it as:
equipped: {
handler () {
this.isEquipped = !this.isEquipped;
},
immediate: true
}
This will change the value of this.isEquipped whenever the value of equipped will change.
I am not sure what is the use case of isEquipped but seeing your code you can use the props directly unless there is a situation where you want to mutate the isEquipped that is not related to the props.
Why not just use a computed value instead?
{
// ...
computed: {
isEquipped () {
// loaded from the component's props
return this.equipped
}
}
}
You can then use isEquipped in your components just as if it was defined in your data() method. You could also just use equipped in your components directly as you don't transform it in any way.
<p>Am I equipped? - <b>{{ equipped }}</b></p>
Watchers are "slow" and they operate on vue's next life-cycle tick which can result in hard to debug reactivity problems.
There are cases where you need them, but if you find any other solution, that uses vue's reactivity system, you should consider using that one instead.
The solution using a computed value from #chvolkmann probably also works for you.
There is a imho better way to do this:
export default {
name: "Item",
props: {
Index : Number,
name: String,
desc : String,
bonus: Array,
equipped : Boolean
},
data() {
return {
isEquipped : false
}
},
updated () {
if (this.equipped !== this.isEquipped) {
this.isEquipped = this.equipped
// trigger "onEquip" event or whatever
}
}
}
The updated life-cycle hook is called -as the name suggests- when a component is updated.
You compare the (unchanged) isEquipped with the new equipped prop value and if they differ, you know that there was a change.
This question already has answers here:
Access vue instance/data inside filter method
(3 answers)
Closed 2 years ago.
I'm creating a simple Vuejs div component (to show a specific value) which needs to receive: a lists, a placeholder and a value as props. What I'm trying to do is displaying the value with the data from my database, if the user picks a new value from the lists, it should take that new value and display it. However, if the user never picks a new value and the data from the database is empty, it should display the placeholder.
So I have used filters to achieve this. However, it outputs an error: "Cannot read property 'lists' of undefined", which comes from the filters (I know because it outputs no error if I comment out the filters). When I changed the filter to this:
filters: {
placeholderFilter () {
return this.placeholderText || this.placeholder
}
}
It says:""Cannot read property 'placeholderText' of undefined"". So I was wondering if the filters properties executed before the data and props properties. What is the execution order of them? I have attached some of the relevant code down below. Anyway, If you could come up with a better way to achieve this. I would appreciate it!
Here is my component:
<template>
<div>{{ placeholderText | placeholderFilter }}</div>
<li #click="pickItem(index)" v-for="(list,index) in lists" :key="index">{{ list }}</li>
</template>
<script>
export default {
props: {
lists: {
type: Array,
required: true
},
value: {
type: [String, Number],
default: ''
},
placeholder: {
type: String,
default: ''
}
},
data () {
return {
selected: -1,
placeholderText: this.value || this.placeholder
}
},
methods: {
pickItem (index) {
this.selected = index
}
},
filters: {
placeholderFilter () {
return this.lists[this.selected] || this.placeholderText || this.placeholder
}
}
}
</script>
And this is where I use it:
<my-component
placeholder="Please type something"
value="Data from database"
lists="['option1','option2','option3']"
>
</my-component>
Filters aren't bound to the component instance, so they simply don't have access to it through the this keyword. They are meant to always be passed a parameter and to return a transformed version of that parameter. So in other words, they're just methods. They were removed in Vue 3 entirely probably for that reason.
And yeah, what you're looking for here is a computed!
TL;DR
I am trying to dynamically build a UI from JSON. The JSON represents a vue.js app with application state (variables) & UI building logic conditional on those variables.
The JSON object of "type": "switch" (see the fiddle linked below), directs the vue.js app to display one of many "cases": {"case1": {..}, "case2": {..}} depending on the value of a state variable "variable": "key" /*translates to vueApp.key */.
Changing one of the variables (update_status) leads to DOM update initially. Changing it again after mounting the app does not affect the DOM, sadly. I'm pretty sure I am doing something stupid or missing something subtle.
Slightly longer version:
(If you're still reading this, please look at the fiddle at this point. None of the below will make sense without it. Thanks!)
Vue.js Template (with app.variables.update_status = "available")
<script type="text/x-template" id="template-switch">
<div>
<!-- Debug statements -->
Switch cases: {{data.cases}}<br>
Variables: {{$root.variables}}
<div v-for="(value, key) in data.cases">
<div v-bind:class="$root.variables[data.variable]"
v-if="key == $root.variables[data.variable]">
<all-components v-bind:data="value"></all-components>
</div>
</div>
</div>
</script>
Input JSON (bound as data in the above template):
{
// Switch on value of app.variables.update_status
"type": "switch",
"variable": "update_status", // Refers to app.variables.update_status
// Used in <script id="template-switch">
"cases": {
// if app.variables.update_status == "checking" (Initial value)
"checking": {
"type": "paragraph",
"text": "Checking for updates"
},
// if app.variables.update_status == "available" (Changed below)
"available": {
"type": "paragraph",
"text": "Updates available."
}
}
}
My question:
Assuming app is the Vue.js app, I'd expect setting app.variables.update_status = "available" should lead to DOM change. But it doesn't as described in TL;DR section. I'm hoping to understand why.
What I have tried:
Made the app watch the entire hierarchy under the object.
I initially thought this is because Vue is unable to monitor object[key] expression where key can change. But its definitely able to do it.
Last value set before mounting the app shows up. After the app is mounted, any change to the the variable sticks but doesn't trigger DOM update. (Annotated with "Doesn't work!" in the fiddle.)
Try it out!
Here's the JS Fiddle (heavily downsized, and commented for easier understanding :))
What to try:
Once the fiddle runs, open the browser console and try executing the following statements:
DEBUG.variables.update_status = "available";
DEBUG.variables.update_status = "checking";
Vue.js version: 2.5.16
Update
Also, I just found out that if I pass data object as:
new Vue({.., data: { .. , variables: {update_status: "temp"}}})
– it works!
I don’t understand this, primarily because variables field is set up to have a deep watcher. I’d assume that when it would have a its fields updated (such as variables.update_status = "new-value";), the observer would eventually trigger the DOM update. But for some reason this doesn’t happen.
I’m really hoping I’m doing something stupid, and that this isn’t this a bug.
Link to the new Fiddle that shows this behaviour: https://jsfiddle.net/g0z3xcyk/
The reason it won't update in your first fiddle is because Vue doesn't detect property addition or deletion, and you're not passing the update_status property when you instance vue, the docs explain it further.
In your second fiddle you're setting update_status when you instance vue and that's why changes, in that case, are detected.
Another option, as mentioned in the docs, is using Vue.set or recreating the object entirely by assigning it again with Object.assign
Some issues with your code:
Check Reactivity In depth as #LuisOrduz commented & answered, Vue cannot detect property addition or deletion. so two solutions:
decalare it first (as your second fiddle did), or uses Vue.set or vm.$set to add one property.
Use vm.$mount(selector) instead of using JQuery to append vm.$el; check vm.$mount
It's better to use vm.$data to access data property instead of vm[key]; check vm.$data
Below is one demo:
function registerComponents() {
Vue.component('all-components', {
template: '#template-all-components',
props: ['data']
});
Vue.component('weave-switch', {
template: '#template-switch',
props: ['data'],
methods: {
toggleStatus: function () {
this.$root.$data.variables.update_status += ' #'
}
}
});
Vue.component('paragraph', {
template: '#template-paragraph',
props: ['data']
});
}
function GenericCard(selector, options) {
var data = Object.assign({}, options.data, {variables: {}});
var watch = {};
Object.keys(data).forEach(function(key) {
watch[key] = {handler: function(val) {
}, deep: true};
});
var app = new Vue({
template: options.template,
data: function () { // uses function instead
return data
},
watch: watch
});
DEBUG = app;
return {
load: function(data) {
Object.keys(data).forEach(function(key) {
app.$data[key] = data[key];
});
//app.$data.variables.update_status = "checking"; // orginal method
app.$set(app.$data.variables, 'update_status', 'checking') // new solution
app.$mount(selector);
//var dom = app.$el;
//$(selector).append(dom); // uses vm.$mount(selector) instead
DEBUG.$set(DEBUG.$data.variables, 'update_status', 'available') // new solution
//DEBUG.$data.variables.update_status = 'available1' // or new solution
//DEBUG.variables.update_status = "available"; // orginal method
},
DEBUG: DEBUG
};
}
registerComponents();
card = GenericCard('#app', {
template: "#template-card",
data: {
ui: {}
}
});
card.load({
ui: {
// Switch on value of app.variables.update_status
"type": "switch",
"variable": "update_status", // Refers to app.variables.update_status
// Used in <script id="template-switch">
"cases": {
// if app.variables.update_status == "checking" (Initial value)
"checking": {
"type": "paragraph",
"text": "Checking for updates"
},
// if app.variables.update_status == "available" (Changed below)
"available": {
"type": "paragraph",
"text": "Updates available."
}
}
}
});
Vue.config.productionTip = false
function toggleStatus() {
card.DEBUG.$data.variables.update_status += ' #'
}
<script src="https://unpkg.com/vue#2.5.16/dist/vue.js"></script>
<script type="text/x-template" id="template-all-components">
<div v-if="data.type == 'paragraph'">
<paragraph v-bind:data="data.text"></paragraph>
</div>
<div v-else-if="data.type == 'switch'">
<weave-switch v-bind:data="data"></weave-switch>
</div>
</script>
<script type="text/x-template" id="template-switch">
<div>
<!-- Debug statements -->
Switch cases: {{data.cases}}<br>
Variables: {{$root.variables}}
<button #click="toggleStatus()">Toggle</button>
<div v-for="(value, key) in data.cases">
<div v-bind:class="$root.$data.variables[data.variable]"
v-if="key == $root.$data.variables[data.variable]">
<all-components v-bind:data="value"></all-components>
</div>
</div>
</div>
</script>
<script type="text/x-template" id="template-paragraph">
<p>{{data}}</p>
</script>
<script type="text/x-template" id="template-card">
<all-components v-bind:data="ui"></all-components>
</script>
<div id="app">
</div>
<button onclick="toggleStatus()">Toggle2</button>
I'm coordinating input elements with the keys of an object on Vuex state. Let's say I've got this object:
myObj: { foo: 1, bar: 2 }
And a computed property in a component:
myObjVals: {
get(){ return this.$store.state.myObj },
set(//?) { //? },
},
In the template, I can get values from the store:
<input type="number" v-model="myObjVals['foo']"/> // displays "1"
<input type="number" v-model="myObjVals['bar']"/> // displays "2"
But when I adjust the value on an input, I get the error: "Do not mutate vuex store state outside mutation handlers."
One obvious solution here is to have a different computed property for each key in myObj... but for larger objects, this gets repetitious/cumbersome. I am wondering if there is any way to code this as I am attempting to above, that is, using only one computed property to reference the object for both get and set functions in v-model.
Come to think of your problem once more. One solution could be something suggested here: https://vuex.vuejs.org/guide/forms.html
<input type="number" :value="myObjVals['foo']" #input="changeMyObj('foo', $event)"/> // displays "1"
<input type="number" :value="myObjVals['bar']" #input="changeMyObj('bar', $event)"/> // displays "2"
with computed property and a method:
computed: {
myObjVals: function () {
return this.$store.state.myObj
}
},
methods: {
changeMyObj(key, evt) {
this.$store.commit('changeMyObj', {key: key, value: evt.data})
}
}
With a mutation inside the store:
mutations: {
changeMyObj(state, objInput) {
state.myObj[objInput.key] = objInput.value
}
}
Here is a "working" fiddle: http://jsfiddle.net/zfab6tzp/346/
Not sure if this is the best solution possible, but it seems to be working