Make a reactive component with vuejs - javascript

I need a Vue component to show some HTML content in v-data-table from Vuetify. I have seen this post Vue 2 contentEditable with v-model, and I created a similar code shown below.
My problem is the component is not reactive. When I click the "Test button", no content is updated in HtmlTextArea.
<template>
<div>
<v-btn #click="doTest()">Test Button</v-btn>
<HtmlTextArea
v-model="content"
style="max-height:50px;overflow-y: scroll;"
></HtmlTextArea>
</div>
<template>
export default {
name: "ModelosAtestados",
components: { HtmlTextArea },
data: () => ({
content: "",
}),
methods: {
doTest() {
this.content = "kjsadlkjkasfdkjdsjkl";
},
},
};
//component
<template>
<div ref="editable" contenteditable="false" v-on="listeners"></div>
</template>
<script>
export default {
name: "HtmlTextArea",
props: {
value: {
type: String,
default: "",
},
},
computed: {
listeners() {
return { ...this.$listeners, input: this.onInput };
},
},
mounted() {
this.$refs.editable.innerHTML = this.value;
},
methods: {
onInput(e) {
this.$emit("input", e.target.innerHTML);
},
},
};
</script>

This occurs because HtmlTextArea sets the div contents to its value prop only in the mounted lifecycle hook, which is not reactive.
The fix is to setup a watcher on value, so that the div contents are updated to match whenever a change occurs:
// HtmlTextArea.vue
export default {
watch: {
value: {
handler(value) {
this.$refs.editable.innerHTML = value;
}
}
}
}
demo

In the #click event binder, you have to pass a function. You passed the result of an executed function.
To make it work: #click="doTest" or #click="() => doTest()".
How to debug such problems:
Display the value you want to update on your template to check if its updated: {{content}}
Use the vue devtool extension to check the current state of your components

Related

VueJs 3 - Custom Input Component

I'm trying to build a custom HTML <input> component for VueJS3. I've been following this tutorial:
https://dev.to/viniciuskneves/vue-custom-input-bk8
So far I managed to get the CustomInput.vue component to work and emit the modified value back to the parent App.Vue.
<template>
<label>
{{ label }}
<input type="text" :name="name" :value="value" #input="onInput" #change="onChange" />
</label>
</template>
<script>
export default {
name: 'CustomInput',
props: {
label: {
type: String,
required: true,
},
value: {
type: String,
required: true,
},
},
computed: {
name() {
return this.label.toLowerCase();
},
},
methods: {
onInput(event) {
this.$emit('input', event.target.value);
},
onChange(event) {
this.$emit('change', event.target.value);
},
},
}
</script>
What I don't understand is - how will the emitted events be detected by the parent App.vue component? I can't see it happens, and I can't find it in the tutorial.
My App.Vue looks like this:
<template>
<custom-input :label="'Name'" :value="name"></custom-input>
<div>{{ name }}</div>
</template>
<script>
import customInput from "./components/CustomInput.vue";
export default {
components: { customInput },
name: "App",
data: function () {
return {
name: "",
};
},
mounted() {
this.name = "Thomas";
},
};
</script>
Thanks in advance for any help :-)
This tutorial is for Vue 2 - for Vue 3 there is another tutorial (https://www.webmound.com/use-v-model-custom-components-vue-3/)
Emitting input event works in Vue 2 only - for Vue 3 you will have to emit update:modelValue and also use modelValue as a prop instead of just value.
You can do it right in your template.
<custom-input :label="'Name'" :value="name" #change='name=$event' #input='name=$event'></custom-input>
You can also use a method or computed with setter as well.

Vue not reacting to a computed props change

I am using the Vue composition API in one of my components and am having some trouble getting a component to show the correct rendered value from a computed prop change. It seems that if I feed the prop directly into the components render it reacts as it should but when I pass it through a computed property it does not.
I am not sure why this is as I would have expected it to be reactive in the computed property too?
Here is my code:
App.vue
<template>
<div id="app">
<Tester :testNumber="testNumber" />
</div>
</template>
<script>
import Tester from "./components/Tester";
export default {
name: "App",
components: {
Tester,
},
data() {
return {
testNumber: 1,
};
},
mounted() {
setTimeout(() => {
this.testNumber = 2;
}, 2000);
},
};
</script>
Tester.vue
<template>
<div>
<p>Here is the number straight from the props: {{ testNumber }}</p>
<p>
Here is the number when it goes through computed (does not update):
{{ testNumberComputed }}
</p>
</div>
</template>
<script>
import { computed } from "#vue/composition-api";
export default {
props: {
testNumber: {
type: Number,
required: true,
},
},
setup({ testNumber }) {
return {
testNumberComputed: computed(() => {
return testNumber;
}),
};
},
};
</script>
Here is a working codesandbox:
https://codesandbox.io/s/vue-composition-api-example-forked-l4xpo?file=/src/components/Tester.vue
I know I could use a watcher but I would like to avoid that if I can as it's cleaner the current way I have it
Don't destruct the prop in order to keep its reactivity setup({ testNumber }) :
setup(props) {
return {
testNumberComputed: computed(() => {
return props.testNumber;
}),
};
}

Dynamically changing props

On my app, I have multiple "upload" buttons and I want to display a spinner/loader for that specific button when a user clicks on it. After the upload is complete, I want to remove that spinner/loader.
I have the buttons nested within a component so on the file for the button, I'm receiving a prop from the parent and then storing that locally so the loader doesn't show up for all upload buttons. But when the value changes in the parent, the child is not getting the correct value of the prop.
App.vue:
<template>
<upload-button
:uploadComplete="uploadCompleteBoolean"
#startUpload="upload">
</upload-button>
</template>
<script>
data(){
return {
uploadCompleteBoolean: true
}
},
methods: {
upload(){
this.uploadCompleteBoolean = false
// do stuff to upload, then when finished,
this.uploadCompleteBoolean = true
}
</script>
Button.vue:
<template>
<button
#click="onClick">
<button>
</template>
<script>
props: {
uploadComplete: {
type: Boolean
}
data(){
return {
uploadingComplete: this.uploadComplete
}
},
methods: {
onClick(){
this.uploadingComplete = false
this.$emit('startUpload')
}
</script>
Fixed event name and prop name then it should work.
As Vue Guide: Custom EventName says, Vue recommend always use kebab-case for event names.
so you should use this.$emit('start-upload'), then in the template, uses <upload-button #start-upload="upload"> </upload-button>
As Vue Guide: Props says,
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 change :uploadComplete="uploadCompleteBoolean" to :upload-complete="uploadCompleteBoolean"
Edit: Just noticed you mentioned data property=uploadingComplete.
It is easy fix, add one watch for props=uploadComplete.
Below is one simple demo:
Vue.config.productionTip = false
Vue.component('upload-button', {
template: `<div> <button #click="onClick">Upload for Data: {{uploadingComplete}} Props: {{uploadComplete}}</button>
</div>`,
props: {
uploadComplete: {
type: Boolean
}
},
data() {
return {
uploadingComplete: this.uploadComplete
}
},
watch: { // watch prop=uploadComplete, if change, sync to data property=uploadingComplete
uploadComplete: function (newVal) {
this.uploadingComplete = newVal
}
},
methods: {
onClick() {
this.uploadingComplete = false
this.$emit('start-upload')
}
}
})
new Vue({
el: '#app',
data() {
return {
uploadCompleteBoolean: true
}
},
methods: {
upload() {
this.uploadCompleteBoolean = false
// do stuff to upload, then when finished,
this.uploadCompleteBoolean = true
},
changeStatus() {
this.uploadCompleteBoolean = !this.uploadCompleteBoolean
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<button #click="changeStatus()">Toggle Status {{uploadCompleteBoolean}}</button>
<p>Status: {{uploadCompleteBoolean}}</p>
<upload-button :upload-complete="uploadCompleteBoolean" #start-upload="upload">
</upload-button>
</div>
The UploadButton component shouldn't have uploadingComplete as local state (data); this just complicates the component since you're trying to mix the uploadComplete prop and uploadingComplete data.
The visibility of the spinner should be driven by the parent component through the prop, the button itself should not be responsible for controlling the visibility of the spinner through local state in response to clicks of the button.
Just do something like this:
Vue.component('upload-button', {
template: '#upload-button',
props: ['uploading'],
});
new Vue({
el: '#app',
data: {
uploading1: false,
uploading2: false,
},
methods: {
upload1() {
this.uploading1 = true;
setTimeout(() => this.uploading1 = false, Math.random() * 1000);
},
upload2() {
this.uploading2 = true;
setTimeout(() => this.uploading2 = false, Math.random() * 1000);
},
},
});
<script src="https://rawgit.com/vuejs/vue/dev/dist/vue.js"></script>
<div id="app">
<upload-button :uploading="uploading1" #click="upload1">Upload 1</upload-button>
<upload-button :uploading="uploading2" #click="upload2">Upload 2</upload-button>
</div>
<template id="upload-button">
<button #click="$emit('click')">
<template v-if="uploading">Uploading...</template>
<slot v-else></slot>
</button>
</template>
Your question seems little bit ambiguë, You can use watch in that props object inside the child component like this:
watch:{
uploadComplete:{
handler(val){
//val gives you the updated value
}, deep:true
},
}
by adding deep to true it will watch for nested properties in that object, if one of properties changed you ll receive the new prop from val variable
for more information : https://v2.vuejs.org/v2/api/#vm-watch
if not what you wanted, i made a real quick example,
check it out hope this helps : https://jsfiddle.net/K_Younes/64d8mbs1/

VueJS pass default prop without reference to child component

I've stumbled upon this situation where I want to pass a prop to a child component that will be the default value of the component, but it will only be showed when the initial value is empty.
Parent Component:
<multi-line-input v-model="data.something" placeholder="Enter Something" :default="data.something"/>
Child Component
props: {
value: {
type: String,
default: ''
},
default: {
type: String,
default: ''
},
},
methods: {
emitBlur (e) {
if (!this.value && this.default) {
this.value = this.default
}
this.$emit('blur')
},
emitInput () {
this.$emit('input', this.$el.value)
}
}
So what I am trying to achieve basically, is when the component loads will get the value from v-model it will also receive a default value that shouldn't change, and only used as a value when the actual value is empty on blur
The default will have the initial value of data.something and it should not change!
I tried to get rid of the reference using JSON.parse(JSON.stringify(this.value)) but it doesn't seem to work either!
So if I understand your question correctly, you want this behavior: upon the blur event on your <multi-line-input> component, if the value of the input is empty, then set the value to a default value which is specified by the parent (through a prop).
First of all, it is an error to do this.value = ... in your component. You must not modify props, props pass data from parent to child only, the data passed through props is not yours to modify directly from within the component.
Try something like this:
Vue.component('multi-line-input', {
template: '<input #blur="onBlur" #input="onInput" :value="value">',
props: {
value: {
type: String,
default: '',
},
default: {
type: String,
default: '',
},
},
methods: {
onBlur() {
if (!this.value && this.default) {
this.$emit('input', this.default);
}
},
onInput(e) {
this.$emit('input', e.target.value);
},
},
});
new Vue({
el: '#app',
data: {
user: null,
initialUser: null,
},
created() {
// Pretend that I'm pulling this data from some API
this.user = {
name: 'Fred',
email: 'fred#email.com',
address: '123 Fake St',
};
// Make a copy of the data for the purpose of assigning the
// default prop of each input
this.initialUser = _.cloneDeep(this.user);
},
});
<script src="https://rawgit.com/vuejs/vue/dev/dist/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.5/lodash.min.js"></script>
<div id="app">
<template v-if="user">
<multi-line-input v-model="user.name" :default="initialUser.name"></multi-line-input>
<multi-line-input v-model="user.email" :default="initialUser.email"></multi-line-input>
<multi-line-input v-model="user.address" :default="initialUser.address"></multi-line-input>
</template>
</div>
Or, if you want the default value to be determined by the component instead of the parent (through a prop), you can do something like this instead:
Vue.component('multi-line-input', {
template: '<input #blur="onBlur" #input="onInput" :value="value">',
props: {
value: {
type: String,
default: '',
},
},
created() {
this.def = this.value;
},
methods: {
onBlur() {
if (!this.value && this.def) {
this.$emit('input', this.def);
}
},
onInput(e) {
this.$emit('input', e.target.value);
},
},
});
new Vue({
el: '#app',
data: {
user: null,
},
created() {
// Pretend that I'm pulling this data from some API
this.user = {
name: 'Fred',
email: 'fred#email.com',
address: '123 Fake St',
};
},
});
<script src="https://rawgit.com/vuejs/vue/dev/dist/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.5/lodash.min.js"></script>
<div id="app">
<template v-if="user">
<multi-line-input v-model="user.name"></multi-line-input>
<multi-line-input v-model="user.email"></multi-line-input>
<multi-line-input v-model="user.address"></multi-line-input>
</template>
</div>
However I do not recommend the second approach because the child component instance will only every have one default value for its entire lifetime. Vue reuses component instances whenever possible, so it wouldn't work if Vue were to bind it to a different parent component (how/when would it update its own default state?).

Updating VueJS component data attributes when prop updates

I'm building a VueJS component which needs to update the data attributes when a prop is updated however, it's not working as I am expecting.
Basically, the flow is that someone searches for a contact via an autocomplete component I have, and if there's a match an event is emitted to the parent component.
That contact will belong to an organisation and I pass the data down to the organisation component which updates the data attributes. However it's not updating them.
The prop being passed to the organisation component is updated (via the event) but the data attibute values is not showing this change.
This is an illustration of my component's structure...
Here is my code...
Parent component
<template>
<div>
<blink-contact
:contact="contact"
v-on:contactSelected="setContact">
</blink-contact>
<blink-organisation
:organisation="organisation"
v-on:organisationSelected="setOrganisation">
</blink-organisation>
</div>
</template>
<script>
import BlinkContact from './BlinkContact.vue'
import BlinkOrganisation from './BlinkOrganisation.vue'
export default {
components: {BlinkContact, BlinkOrganisation},
props: [
'contact_id', 'contact_name', 'contact_tel', 'contact_email',
'organisation_id', 'organisation_name'
],
data () {
return {
contact: {
id: this.contact_id,
name: this.contact_name,
tel: this.contact_tel,
email: this.contact_email
},
organisation: {
id: this.organisation_id,
name: this.organisation_name
}
}
},
methods: {
setContact (contact) {
this.contact = contact
this.setOrganisation(contact.organisation)
},
setOrganisation (organisation) {
this.organisation = organisation
}
}
}
</script>
Child component (blink-organisation)
<template>
<blink-org-search
field-name="organisation_id"
:values="values"
endpoint="/api/v1/blink/organisations"
:format="format"
:query="getQuery"
v-on:itemSelected="setItem">
</blink-org-search>
</template>
<script>
export default {
props: ['organisation'],
data() {
return {
values: {
id: this.organisation.id,
search: this.organisation.name
},
format: function (items) {
for (let item of items.results) {
item.display = item.name
item.resultsDisplay = item.name
}
return items.results
}
}
},
methods: {
setItem (item) {
this.$emit('organisationSelected', item)
}
}
}
</script>
How can I update the child component's data properties when the prop changes?
Thanks!
Use a watch.
watch: {
organisation(newValue){
this.values.id = newValue.id
this.values.search = newValue.name
}
}
In this case, however, it looks like you could just use a computed instead of a data property because all you are doing is passing values along to your search component.
computed:{
values(){
return {
id: this.organisation.id
search: this.organisation.name
}
}
}

Categories

Resources