Vue component communication - vue.js

I'm looking for a concise example of two Vue components. The first component should contain a text input or textarea. The second component displays a character counter. I would like the first component to emit change events, and the second component should listen for those events and display its computed values (character count). I'm new to Vue and trying to wrap my head around the best way to implement this functionality. It seems rather straightforward in pure JavaScript but doing it the Vue way is not as clear to me. Thanks.
Here is how I'd do it in JavaScript:
Here's the textarea:
<textarea id="pagetext" name="pagetext"
onChange="characterCount();"
onKeyup="characterCount();">Type here</textarea>
Here's the JavaScript:
function characterCount()
{
var characters=document.myForm.pagetext.value.length;
document.getElementById('charcounter').innerHTML=characters+"";
}
My concern with Vue is passing the entire value around... for performance reasons this seems less than ideal. I may want my text editing Vue component to self-contain the value and emit the stats, ie the value for character count which would then be observed by a text stats component.

You can create a "Model" for value of textarea and provide this model to second component by using following way https://v2.vuejs.org/v2/guide/components-props.html

I've written up a snippet with four examples: your original, a simple Vue app (no components) that does the same thing, and two apps with two components that are coordinated by the parent.
The simple Vue app is actually more concise than the pure JavaScript app, and I think it shows off the reason for having a framework: your view doesn't act as a store for your program data, from which you have to pull it out.
In the final example, the parent still owns pageText, but passes it down to the my-textarea component. I like to hide the emitting behind the abstraction of a settable computed, so that the element can use v-model. Any changes are emitted up to the parent, which changes pageText, which propagates back down to the component.
I think your performance concerns fall into the realm of premature optimization, but it is possible not to use the text content as data at all, and only be concerned with the length. The fourth example does that. emitLength could have used event.target.value.length, but I wanted to use it in the mounted to initialize the length properly, so I used a ref.
function characterCount() {
var characters = document.myForm.pagetext.value.length;
document.getElementById('charcounter').innerHTML = characters + "";
}
new Vue({
el: '#app',
data: {
pageText: 'Type here'
}
});
new Vue({
el: '#app2',
data: {
pageText: 'Type here'
},
components: {
myTextarea: {
props: ['value'],
template: '<textarea name="pagetext" v-model="proxyValue"></textarea>',
computed: {
proxyValue: {
get() {
return this.value;
},
set(newValue) {
this.$emit('input', newValue);
}
}
}
},
textLength: {
props: ['value'],
template: '<div>{{value}}</div>'
}
}
});
new Vue({
el: '#app3',
data: {
textLength: null
},
components: {
myTextarea: {
template: '<textarea ref="ta" name="pagetext" #input="emitLength">Type here</textarea>',
methods: {
emitLength() {
this.$emit('change', this.$refs.ta.value.length);
}
},
mounted() {
this.emitLength();
}
},
textLength: {
props: ['value'],
template: '<div>{{value}}</div>'
}
}
});
<script src="https://unpkg.com/vue#latest/dist/vue.js"></script>
<form name="myForm">
<textarea id="pagetext" name="pagetext" onChange="characterCount();" onKeyup="characterCount();">Type here</textarea>
</form>
<div id="charcounter"></div>
<div id="app">
<h1>Vue (simple)</h1>
<form>
<textarea name="pagetext" v-model="pageText"></textarea>
</form>
<div>{{pageText.length}}</div>
</div>
<div id="app2">
<h1>Vue (with components)</h1>
<form>
<my-textarea v-model="pageText"></my-textarea>
</form>
<text-length :value="pageText.length"></text-length>
</div>
<div id="app3">
<h1>Vue emitting stats</h1>
<form>
<my-textarea #change="(v) => textLength=v"></my-textarea>
</form>
<text-length :value="textLength"></text-length>
</div>

Related

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>

Vue stored valued through props not being reactive

So I pass value using [props] and stored it in child component's data. However, when passing [props] value changes from parent, it's not updating in child component's data. Is there a fix for this..?
Here is the link to w3 test (I tried to clarify the problem as much as possible here)
<div id='app'>
<div id='parent'>
<button #click='current_value()'>Click to see parent value</button>
<br><br>
<button #click='change_value($event)'>{{ txt }}</button>
<br><br>
<child-comp :test-prop='passing_data'></child-comp>
</div>
<br><br>
<center><code>As you can see, this methods is <b>NOT</b> reactive!</code></center>
</div>
<script>
new Vue({
el: "#parent",
data: {
passing_data: 'Value',
txt: 'Click to change value'
},
methods: {
current_value(){
alert(this.passing_data);
},
change_value(e){
this.passing_data = 'New Vaule!!';
this.txt = 'Now click above button again to see new value';
e.target.style.backgroundColor = 'red';
e.target.style.color = 'white';
}
},
components: {
"child-comp": {
template: `
<button #click='test()'>Click here to see child (stored) value</button>
`,
props: ['test-prop'],
data(){
return {
stored_data: this.testProp
}
},
methods: {
test(){
alert(this.stored_data);
}
},
watch: {
stored_data(){
this.stored_data = this.testProp;
}
}
}
}
});
Props have one way data flow, that's why it doesn't react when you update it from the parent component. Define a clone of your prop at data to make it reactive, and then you can change the value within the child component.
Short answer: you don't need stored_data. Use alert(this.testProp) directly.
Long answer: when child component is created, stored_data get it's value from this.testProp. But data is local, it won't change automatically. That's why you need to watch testProp and set it again. But is not working because of a simple mistake, your watch should be:
watch: {
testProp(){ // here was the mistake
this.stored_data = this.testProp;
}
}

How to clone props object and make it non-reactive [duplicate]

This question already has answers here:
What is the most efficient way to deep clone an object in JavaScript?
(67 answers)
Closed 12 days ago.
I have some form data which I share with children components through props. Now I want to clone the prop object and make it non-reactive. In my case I want the user to be able to modify the props value without actually changing the cloned value. The cloned value should only be there to show the user what the form was when editing. Below code shows this:
<template>
<div>
<div v-if="computedFormData">
original prop title: {{orgData.title}}
new title:
<input type="text" v-model="formData.title"/>
//changing data here will also change orgData.title
</div>
</div>
</template>
<script>
export default {
props: ['formData'],
data() {
return {
orgData: [],
}
},
computed: {
computedFormData: function () {
this.orgData = this.formData;
return this.orgData;
},
},
methods: {
},
}
</script>
I have tried with Object.freeze(testData); but it doesnt work, both testData and orgData are reactive. Note also that using mounted or created property does not render orgData so I'm forced to use the computed property.
Try copying the prop values with Object.assign. No more issue with reactivity since the new, assigned values are just the copy instead of the reference to the source.
If your data object is a lot more complex, I'd recommend deepmerge in place of Object.assign.
Vue.component('FormData', {
template: `
<div>
<div v-if="testData">
<p>Original prop title: <strong>{{orgData.title}}</strong></p>
<p>Cloned prop title:</p>
<input type="text" v-model="testData.title" />
</div>
</div>
`,
props: ['orgData'],
data() {
return {
testData: Object.assign({}, this.orgData)
}
}
});
const vm = new Vue({
el: '#app',
data() {
return {
dummyForm: {
title: 'Some title'
}
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<form-data :org-data="dummyForm"></form-data>
</div>
Not entirely sure why but using Object.assign on a computed property did not work for me. I solved it by using a watch property for the props value:
watch:{
formData(){
this.orgData = Object.assign({}, this.formData)
}
},
Object.assign is merely a shallow copy. If you have a copy consists that of only primitive data types (string, number, bigint, boolean, undefined, symbol, and null) it's ok. It to remove its reactivity. But, if you have a copy that has reference types you can’t shallow clone it to remove its reactivity.
For depping clone you can use the JSON.parse(JSON.stringify()) pattern. But keep in mind that is going to work if your data consists of supported JSON data types.
props: ['orgData'],
data() {
return {
cloneOrgData: JSON.parse(JSON.stringify(this.orgData))
}
}

How to make a template variable non-reactive in Vue

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.

vuejs - computed is not working with props

I'm using props to update my site content in the child component. This is basically working like this:
<child-component :updatedArray="updatedArray" />
then in the child component:
<template>
{{updatedArray}}
<div>{{computedArray}}</div>
</template>
<script>
props: ['updatedArray'],
...
computed: {
computedArray() {
if(this.updatedArray.item == "item one") {return "item one"}
else {return "other item"}
}
}
</script>
Now this code should work in any case when I update updatedArray in my parent component. Then I see in my child component that my {{updatedArray}} is changing correctly, but my computedArray is not triggered and does not work.
Can I ask you why is this happening?
Does computed do not work with every props update?
How should I correct my code?
edit: not duplicate
I'm not mutating the prop, I rather only do a computed based on its value.
Your bind uses wrong name.
As Vue Guide describes:
HTML attribute names are case-insensitive, so browsers will interpret
any uppercase characters as lowercase. That means when you’re using
in-DOM templates, camelCased prop names need to use their kebab-cased
(hyphen-delimited) equivalents
So you need to convert camelCase to kebab-case.
like v-bind:updated-array instead of v-bind:updatedArray.
Below is one working demo using kebab-case. You can change it to camelCase, then you will find not working.
Vue.component('child', {
template: '<div><span style="background-color:green">{{ updatedArray }}</span><div style="background-color:red">{{computedArray}}</div></div>',
props: ['updatedArray'],
computed: {
computedArray() {
if(this.updatedArray.item.length > 0) {return this.updatedArray}
else {return "other item"}
}
}
})
new Vue({
el: '#app',
data() {
return {testArray: {
'item': 'test',
'prop1': 'a'
}}
},
methods:{
resetArray: function() {
this.testArray['item'] += this.testArray['prop1'] + 'b'
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<button v-on:click="resetArray()">Click me</button>
<child v-bind:updated-array="testArray"></child>
</div>