How to make a template variable non-reactive in Vue - vue.js

I have an edit form with variables held in the data(). I don't want the title of the edit page to update yet I want to maintain the v-model sync of data between the input and data. What's the simplest way to make the title non-reactive in the h1 tag? Mr You has to have something up his sleeve for this..
<template>
<div>
<h1>{{ title }}</h1>
<input v-model="title">
</div>
</template>
<script>
export default {
data: {
title: 'Initial value'
}
}
</script>

The Vue docs recommend Object.freeze() on the returned object in data() to disable reactivity on properties:
data() {
return Object.freeze({ title: 'Initial value' })
}
But the caveat is it freezes all properties (it doesn't look like there's a way to freeze only some properties using this method), and using v-model with this causes console errors (Cannot assign to read only property).
Vue.config.devtools = false;
Vue.config.productionTip = false;
new Vue({
el: '#app',
data() {
return Object.freeze({
message: 'Hello Vue.js!',
})
}
})
<script src="https://unpkg.com/vue#2.5.17"></script>
<div id="app">
<p>{{ message }}</p>
<input v-model="message"> <!-- XXX: Cannot use v-model with frozen property. This will cause a console error. -->
</div>
Alternatively, you could arbitrarily remove the reactivity from any configurable data property by redefining it with writeable: false:
methods: {
removeReactivity() {
Object.defineProperty(this, 'title', {value: null, writeable: false});
}
}
Vue.config.devtools = false;
Vue.config.productionTip = false;
new Vue({
el: '#app',
data() {
return {
message: 'Hello Vue.js!',
}
},
methods: {
removeReactivity() {
Object.defineProperty(this, 'message', {value: null, writeable: false});
}
}
})
<script src="https://unpkg.com/vue#2.5.17"></script>
<div id="app">
<p>{{ message }}</p>
<input v-model="message">
<div>
<button #click="removeReactivity">
Remove reactivity for <code>message</code>
</button>
</div>
</div>

You could potentially use v-once directive for your purpose if you don't want to create a separate variable for input. From the docs:
Render the element and component once only. On subsequent re-renders,
the element/component and all its children will be treated as static
content and skipped.
new Vue({
el: "#app",
data: {
title: "initial value"
}
})
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.17/dist/vue.min.js"></script>
<div id="app">
<input v-model="title">
<p>Reactive title: {{ title }}</p>
<p v-once>Static title: {{ title }}</p>
</div>

If you don't want the input to change the value of your data item, use value to bind it rather than the two-way v-model. Then it just acts as an initializer for the input.
If you want to have two values, one that doesn't change and one that does that gets initialized from the other, you need to have two data items. The non-changing one can be a prop with a default value. The other is a data member which, if you use a data function, can initialize itself to the prop value.
new Vue({
el: '#app',
props: {
initTitle: {
default: 'Initial value'
}
},
data() {
return {
title: this.initTitle
};
}
});
<script src="https://unpkg.com/vue#latest/dist/vue.js"></script>
<div id="app">
<h1>{{ initTitle }}</h1>
<input v-model="title">
<div>Title is "{{title}}"</div>
</div>
You could alternatively use the little-known $options properties to define your title as a sort of internal constant rather than a prop. I am of mixed feelings about whether this is a good design approach or a step too weird.
new Vue({
el: '#app',
initTitle: 'Initial value',
data() {
return {
title: this.$options.initTitle
};
}
});
<script src="https://unpkg.com/vue#latest/dist/vue.js"></script>
<div id="app">
<h1>{{ $options.initTitle }}</h1>
<input v-model="title">
<div>Title is "{{title}}"</div>
</div>

Working backwards from the contents of this blog...
It appears that when you create an object for Vue, it creates the properties with reactive getters and setters. If you then append a property to that object out-of-band, then it won't get the reactive capability, but will still be accessible as a value.
This should solve it for you:
<template>
<div>
<h1>{{ titleContainer.value }}</h1>
<input v-model="title">
</div>
</template>
<script>
export default {
data: {
titleContainer: {}
}
}
titleContainer.value = "Initial Value"
</script>

There is no easy way to solve your problem with Vue as is since Vue automatically injects reactive getters and setters for all object properties. You could use Object.freeze() on the variable to remove reactivity BUT it would apply across the whole object itself which is not what you want.
I created a fork out of vue called vue-for-babylonians to restrict reactivity and even permit some object properties to be reactive. Check it out here.
With it, you can tell Vue to not make any objects which are stored in vue or vuex from being reactive. You can also tell Vue to make certain subset of object properties reactive. You’ll find performance improves substantially and you enjoy the convenience of storing and passing large objects as you would normally in vue/vuex.

Related

Vue mutate prop binded by v-bind with sync modifier

I have an object in my component data. Now, I'm just binding all the properties of the object as a prop to the child component using v-bind.sync directive. I'm updating these props from the child component using the built-in update event but still, I'm getting Avoid mutation props directly error in the console. Here is the minimal example attached.
Parent Component
<template>
<div>
<oslo v-bind.sync="data" />
</div>
</template>
<script>
import Oslo from '#/components/Oslo.vue'
export default {
components: {
Oslo,
},
name: 'OsloParent',
data() {
return {
data: {
data: {
name: 'Oslo name',
access: 'admin'
}
},
}
},
}
</script>
Child component
<template>
<div>
<input type="text" v-model="name" #keyup="$emit('update:name', name)" />
<input type="text" v-model="access" #keyup="$emit('update:access', access)" />
</div>
</template>
<script>
export default {
props: {
name: String,
access: String
},
name: 'Oslo',
}
</script>
This is just an example component I've created for the reproduction of the problem. The actual component is supposed to handle so many props with two-way binding and that's the reason I'm binding the data with v-bind directive with sync modifier. Here is the Vue warning from the console (most common).
[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: "name"
Any suggestions to improve this or silent the Vue warn for this specific case? The above-given components works as desired, Vue throws error though.
I found two problems with your example that might throw this off.
The use of v-model directly to the property. Use v-bind instead to have it only display. And use v-on:change handler to fire the $emit('update:propertyname', value) and send the new value to update on the object.
The value sent along in the $emit seems empty and thus makes no change. Use $event.target.value instead.
Side note: v-on:keyup might not be the best event to listen to, since input can also be drag-and-dropped. Listening to v-on:change would be beter in that case.
Note on event listeners when using only v-bind.sync instead of v-bind:propertyName.sync:
If you want to listen to the update:propertyName event from the child component on the parent, you have to use the .capture modifier. Otherwise the update event is caught by the v-on:update:propertyName on the child component and this does not bubble up to the parent.
So you can use v-on:update:name.capture="someMethod" on the <oslo> tag for example. And have this someMethod in the parent's methods. After this is called, the event will be triggered on the child component which will update the object and thereby the property.
All together:
let Oslo = {
props: {
name: String,
access: String
},
name: 'Oslo',
template: `<div>
<input type="text" :value="name" #change="$emit('update:name', $event.target.value)" />
<input type="text" :value="access" #change="$emit('update:access', $event.target.value)" />
</div>`
}
new Vue({
el: "#app",
components: {
Oslo,
},
data: {
thedata: {
name: 'Oslo name',
access: 'admin'
}
},
methods: {
nameWillBeUpdated: function(v) {
console.log('New value of name will be:', v);
// After this, the `update:name` event handler of the
// child component is triggered and the value will change.
},
},
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="app">
<span>{{this.thedata.name}} - {{this.thedata.access}}</span>
<oslo
v-bind.sync="thedata"
v-on:update:name.capture="nameWillBeUpdated"
/>
</div>
You can just pass an object and sync it instead of individual properties if you have many properties to listen to from child component. See the example below:
Vue.config.productionTip = false
Vue.config.devtools = false
Vue.component('Oslo', {
template: `
<div>
<input type="text" v-model="comp_name" #keyup="$emit('update:name', comp_name)" />
<input type="text" v-model="comp_access" #keyup="$emit('update:access', comp_access)" />
</div>
`,
props: {
data: {
name: String,
access: String,
}
},
data() {
return {
comp_name: this.data.name,
comp_access: this.data.access
}
}
})
new Vue({
el: '#app',
data() {
return {
doc: {
name: 'Oslo name',
access: 'admin'
}
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div>
<span>---- {{ this.doc.name }}----</span>
<span>---- {{ this.doc.access }}----</span>
<oslo :data="this.doc" v-bind.sync="doc" />
</div>
</div>

How to display stub component when component is not found in vue

I am trying to catch situation, when component is not found, ie:
{
template: '<some-unknown-component></some-unknown-component>'
}
At that moment, Vue warns us with unknown custom element: <some-unknown-component>...
I would like to step in when some-unknown-component is not found and then use another component instead, like stub-component:
{
name: 'stub-component',
props: ['componentName'],
template: '<p>component ${componentName} does not exists, click here to create...</p>'
}
UPDATE: I am looking for solution without changing the template itself, so no v-if and component added.
Vue exposes a global error and warning handler. I managed to get a working solution by using the global warnHandler. I don't know if it is exactly what you are looking for, but it may be a good starting point. See the working snippet (I think it is quite self explanatory).
Vue.config.warnHandler = function (err, vm, info) {
if (err.includes("Unknown custom element:")) {
let componentName = err.match(/<.*>/g)[0].slice(1, -1)
vm.$options.components[componentName] = Vue.component('stub-component', {
props: ['componentName'],
template: `<p>component "${componentName}" does not exists, click here to create...</p>`,
});
vm.$forceUpdate()
} else {
console.warn(err)
}
};
new Vue({
el: '#app',
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<unknown-component></unknown-component>
</div>
Vue stores the details of all the registered components in the $options.component property of the Vue instance.
So, you can check for the component availability using this.$options.component and if the component is present then load the component otherwise load the other component.
In the below example, suppose you have two different components and you want to load them on the availability, then you can create a computed property on the basis of it, load the component as needed.
var CustomComponent = Vue.extend({ template: '<h2>A custom Component</h2>' });
var AnotherComponent = Vue.extend({ template: '<h2>Custom component does not exist.</h2>' });
new Vue({
el: "#app",
components: {
CustomComponent,
AnotherComponent
},
computed: {
componentAvailable () {
return this.$options.components.CustomComponent
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-if="componentAvailable">
<custom-component />
</div>
<div v-else>
<another-component />
</div>
</div>

Why is Vue changing the parent value from a child without emitting an event

I am fairly new to Vue but doesn't this behavior completely contradict the design of props down, events up?
I have managed to stop it by using Object.assign({}, this.test_object ); when initializing the value in child-component but shouldn't that be the default behaviour?
Here is some background.
I am trying to have a dirty state in a much larger application (Eg a value has changed so a user must save the data back to the database before continuing on their way)
I had an event being emitted, and caught by the parent but the code I had to test the value and init the dirty state was not running as the value had already been changed in the parent component.
Vue.component( 'parent-component', {
template: '#parent-component',
data: function() {
return {
testObject: {
val: 'Test Value'
}
}
}
});
Vue.component( 'child-component', {
template: '#child-component',
props: {
test_object: Object
},
data: function() {
return {
child_object: this.test_object
}
}
});
Vue.config.productionTip = false;
new Vue({
el: '#app',
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script type="text/x-template" id="parent-component">
<div>
<child-component :test_object="testObject"></child-component>
<p>This is in the parent component</p>
<p><code>testObject.val = {{testObject.val}}</code></p>
</div>
</script>
<script type="text/x-template" id="child-component">
<div>
<label for="html_input">HTML Input</label>
<input style="border:1px solid #CCC; display:block;" type="text" name="html_input" v-model="child_object.val" />
</div>
</script>
<div id="app">
<parent-component></parent-component>
</div>
Use of v-model is a very deceptive thing. If you are not careful, you might end-up mutating data that doesn't belong to your component. In your case, you are accidentally passing read-only props directly to the v-model. It doesn't know if it is a prop or a local component state.
What you are doing is the right solution but considering one-way/unidirectional data flow in mind, we can rewrite this example in more explicit and elegant fashion:
Your component definition would be:
Vue.component( 'parent-component', {
template: '#parent-component',
data: function() {
return {
testObject: {
val: 'Test Value'
}
}
},
methods: {
// Added this method to listen for input event changes
onChange(newValue) {
this.testObject.val = newValue;
// Or if you favor immutability
// this.testObject = {
// ...this.testObject,
// val: newValue
// };
}
}
});
Your templates should be:
<script type="text/x-template" id="parent-component">
<div>
<child-component :test_object="testObject"
#inputChange="onChange"></child-component>
<p>This is in the parent component</p>
<p><code>testObject.val = {{testObject.val}}</code></p>
</div>
</script>
<!-- Instead of v-model, you can use :value and #input binding. -->
<script type="text/x-template" id="child-component">
<div>
<label for="html_input">HTML Input</label>
<input type="text" name="html_input"
:value="test_object.val"
#input="$emit('inputChange', $event.target.value)" />
</div>
</script>
Key things to note:
When using v-model, ensure that you are strictly working on a local value/data of the component. By no means, it should be referenced copy of external prop.
A custom form-like component can be readily converted into the one that can work with v-model provided you accept current value as :value prop and event as #input. v-model will just work out of the box.
Any modification to the value should happen in the same component.

How to pass data into components in vue.js?

I have this
<template id="vButton">
<button v-bind:title="name">{{name}}</button>
</template>
<div id="app">
<ti-button></ti-button>
</div>
js
Vue.component('ti-button', {
props: ['name'],
template: '#vButton'
});
var vm2 = new Vue({
el: '#app',
data: {
name : 'hi'
}
});
I want the button to have innerText and title attribute to say 'hi'. But it does not. Does anyone know why?
Ref: https://v2.vuejs.org/v2/guide/components.html
Thanks
Vue.component('ti-button', {
props: ['name'],
template: '#vButton'
});
var vm2 = new Vue({
el: '#app',
data: {
name: 'hi'
}
});
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.17/dist/vue.js"></script>
<script type="text/x-template" id="vButton">
<div>
<button>{{name}}</button>
<button>{{$root.name}}</button>
</div>
</script>
<div id="app">
<ti-button name="first_button"></ti-button>
</div>
UPD:
Do you mean you don't see this?
You are doing things mostly right, you just need to pass your data to your prop using a v-bind:
<ti-button v-bind:name="name"></ti-button>
Static values can be passed without v-bind but for dynamic values like you are attempting to pass you need to bind the prop. Check out the static/dynamic prop documentation for more information.
And here is a working demo: https://codepen.io/egerrard/pen/qJpzMQ

How to pass multiple props from parent to child component in Vue

I'm trying to pass two properties from parent to child, but for some reason this isn't working and all the examples I've found refer to passing a single property. What I've tried to do is:
Parent vue component:
<template>
<div class="statistics_display">
<multiLineChart :rowsA="reading['A'].price_stats" :rowsB="reading['B'].price_stats"></multiLineChart>
</div>
</template>
multiLineChart vue component:
export default {
name: 'MultiLineChart',
props: ['rowsA', 'rowsB'],
mounted: function() {
console.log(this.rowsA);
}
the console log is returning undefined. If I executethe exact same code and pass a single prop, it returns the expected prop contents. What am I missing?
HTML attributes are case-insensitive, so
<multiLineChart :rowsA="reading['A'].price_stats" :rowsB="reading['B'].price_stats"></multiLineChart>
Are actually bound to props: ['rowsa', 'rowsb'].
If you want props: ['rowsA', 'rowsB']to work, use, in the template: :rows-a="..." and :rows-b="...".
See it working below.
Vue.component('multilinechart', {
template: "#mtemplate",
props: ['rowsA', 'rowsB'],
mounted: function() {
console.log(this.rowsA, this.rowsB);
}
})
new Vue({
el: '#app',
data: {
reading: {A: {price_stats: 11}, B: {price_stats: 22}}
}
});
<script src="https://unpkg.com/vue#2.5.13/dist/vue.min.js"></script>
<div id="app">
<div class="statistics_display">
<multiLineChart :rows-a="reading['A'].price_stats" :rows-b="reading['B'].price_stats"></multiLineChart>
</div>
</div>
<template id="mtemplate">
<div>I'm multilinechart</div>
</template>