I recently experienced a bug in my vue.js application. I was able to fix it, and it taught me something about how vue.js works. I want to know if I'm corret.
First of all, the bug:
props: {
property: {},
...
}
data(): {
return {
propertyData: {
field1: null,
field2: '',
field3: [{email: null, notifying: false}]
},
...
};
}
created() {
this.propertyData = JSON.parse(JSON.stringify(this.property));
}
I was trying to make a copy of the property prop and assigned that to propertyData as a means of being able to mutate propertyData without mutating the property prop directly. But this was causing issues in the template. It almost seemed like there was a disconnect between the propertyData object and whatever the template was binding to.
For example, I would add this to the template:
{{propertyData.field3}}
...and it would print to the screen:
[{email: null, notifying: false}]
...which is expected at first. But I have an email text field that binds to the email field in each object of field3:
<v-layout v-for="(item, index) in propertyData.field3" :key="index">
<v-flex>
<v-text-field
label="Email"
v-model="item.email"
placeholder="username#example.com"
:rules="[v => !!v || 'Email is required',
v => /.+#.+/.test(v) || 'Email is invalid' ]"
maxlength="255"
required>
</v-text-field>
</v-flex>
</v-layout>
When I type something into the email field, the propertyData array doesn't update on the screen... that is, until the email field is validated (for example, when I finally add # and the first character of the domain).
I would put console logs in the code and it would print out the correct output for propertyData. That is, it would print out exactly what I currently had in the email field even though, on the screen, it didn't.
Then I tried this instead:
created() {
Object.assign(this.propertyData, {...this.property});
}
This fixed the problem. Now the propertyData.field3 array prints the actual current data entered into the email field and updates immediately.
So the lesson I learned from this (and this is what I'd like someone to confirm) is that the object the template binds to is not the data object (not directly at least). Once the component is created (or maybe mounted), any references to propertyData in the template, or to any of its fields, refers to the initial object defined in data() (with null for field1, '' for field2, etc.). But if you assign a completely different object to propertyData after that (which is what this.propertyData = JSON.parse(JSON.stringify(this.property)) would do), the template doesn't change the object it binds to. It's still bound to the initial object (with null for field1, '' for field2, etc.) and it requires a rendering update (which validation would do) to update the object the template binds to to the object propertyData refers to. My fix worked because Object.assign(...) doesn't change the object propertyData refers to, it just changes (or adds to) the fields.
^ Is this correct?
Related
I am using vue-multiselect like so:
<multiselect
id="customer_last_name_input"
v-model="value"
:options="activeUserProfiles"
label="lastname"
placeholder="Select or search for an existing customer"
track-by="uid"
:close-on-select="true"
#select="onSelect"
#remove="onRemove"
:loading="isLoading"
:custom-label="customerSelectName"
aria-describedby="searchHelpBlock"
selectLabel=""
>
...that grabs the list of active customers from an Array and then makes them available in a nice select menu.
This works good. However, I need to add another option from another resource (called customerNone) to the options prop and but the data is returned as an Object like so:
{"uid":1,"lastname":"None Given","firstname":"User","email":null,"phone":null...blah}
The vue-multiselect docs state that the :option prop MUST be an Array.
Question: What is the best way for me to handle this in the vue-multiselect component? Here's my attempt to help explain what I am trying to do (not sure if this is the best way to handle it). Unfortunately, my attempt causes a console error (see below):
I am passing a prop down called noCustomer which, if is true, I need to use customerNone profile on :options:
<multiselect
:options="noCustomer ? customerNone : getActiveUserProfiles"
>
here's the error:
Invalid prop: type check failed for prop "options". Expected Array, got Object
Is there a way I can convert the customerNone object to an array of object? Thanks!
You could wrap the customerNone object in brackets at the time that you pass it to the <multiselect> like [customerNone].
This syntax creates a new array on the fly, having 1 element that is the object variable:
<multiselect
:options="noCustomer ? [customerNone] : getActiveUserProfiles"
>
Update for comments
In order to auto-select the generic option when it's available, use a watch on the noCustomer prop to set value whenever noCustomer === true:
watch: {
noCustomer(newValue, oldValue) {
if(newValue) { // Checking that `noCustomer === true`
this.value = this.customerNone;
}
}
}
I have a dynamic form which contains a list of elements specified in the following way:
element1: { name: "property1", value: "value1", defaultValue: "defaultvalue1", usingDefaultValue: true, type: "String/Enum/Int/Long }
This list of elements is constantly published via a WebSocket and stored in a Vuex store. Each element corresponds to an input, which displays either the value or the defaultValue, depending on a toggle switch. These values are bound to the input fields using v-model.
I use the vuex-map-fields library to help with the dynamic multi row fields. They are defined as follows:
computed: {
...mapMultiRowFields(`elementsStore`, ["elements"])
},
The fields are passed into a list component using a v-for:
<v-layout v-for="element in elements" :key="element.name">
<element-list-item :element="element"></element-list-item>
</v-layout>
Now, the following works perfectly as I expect:
<v-text-field
v-model="element.useDefaultValue ? element.defaultValue : element.value"
</v-text-field>
However, whenever I use npm run lint, I get the following error message:
error: 'v-model' directives require the attribute value which is valid as LHS (vue/valid-v-model)
Is there a better way to achieve this same behaviour?
I'm going to assume the error message is there for a good reason.
One alternative approach I have tried is using a computed property. However, this did not work and immediately displayed errors in the console.
I am formatting a input into currency and using writable computed variables to update the value back to textbox.
e.g. I have a value 1234.12
I am copying the value from notepad and pasting it into the textbox, on tab out it is hitting the read function and getting formatted into currency and getting written back to textbox as $1,234.
When I am pasting same value 1234, its is not hitting the read, directly getting written as it is, in to the textbox as 1234 on tab out.
I have seen this problem in js also.
Do you have any idea how to format the value if I paste the same value multiple times.
You can use the { notify: "always" } extender to ensure your data always gets formatted.
In the example below:
The _backingValue contains "1234"
When inputing "1234" in the <input>, the computed writes "1234" to the _backingValue
Under normal conditions, the _backingValue would not notify any subscribers of a value change, since "1234" === "1234. However, because we explicitly told it to always trigger a valueHasMutated "event", it does notify its subscribers.
formattedValue's read property has a dependency on _backingValue.
Because _backingValue notifies us it has changed, the formatting function will run again, outputting "1234$".
Under normal conditions, formattedValue would not notify any subscribers of a value change, since "1234$" === "1234$". Again however, because of the extension, a valueHasMutated is triggered.
The <input>'s value binding receives an update and renders "1234$" to the screen.
const _backingValue = ko.observable("1234")
.extend({ notify: "always" });
const formattedValue = ko.computed({
read: () => _backingValue().replace(/\$/g, "") + "$",
write: _backingValue
}).extend({ notify: "always" });
ko.applyBindings({ formattedValue });
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<input data-bind="value: formattedValue">
If I have data like so in Vue JS 2:
data : {
newBus: {
name: '',
hours: {
sunday: '',
}
}
}
And I set it here:
<input v-model="newBus.hours.sunday" type="text" placeholder="Sunday">
Adding a business this way works, the issue comes when I try to update them. The user clicks on the item from a list and then the saved data fills in the form like so:
<div v-for="bus in businesses" v-on:click="editBus(bus)" class="list-group-item bus-list-item">{{bus.name}}</div>
Method:
editBus: function (bus) {
/*Set post values to form*/
this.newBus = bus
},
But I get the error:
vue.js:1453 TypeError: Cannot read property 'sunday' of undefined
This happens immediately when the item to be updated is clicked. Any ideas?
EDIT: It appears to be related to the hours property not being avaliable on bus. If I console.log(bus) it doesn't show. But I need to add this data to this business. Is there a way to have it ignore data it doesn't yet have? What I don't understand is that if it was not nested...aka sunday: '' instead of hours: { sunday: ''} it works fine.
EDIT: Here is the CodePen recreation.
The problem here isn't really a Vue problem it's a Javascript problem. You cannot access a property of undefined.
Using the example data structure from your pen.
newComment: {
name: '',
comment: '',
votes: 0,
time: timeStamp(),
subcomment: {
name: ''
}
}
In this case, you've set everything up with a defined value, so everything is going to be resolved. The problem comes when you are clicking your edit button, the object received looks like this:
{
name: "some name",
comment: "some comment",
time: "4/5/2017",
votes: 145,
.key: "-Kh0PVK9_2p1oYmzWEjJ"
}
Note here that there is no subcomment property. That means that the result of this code
newComment.subcomment
is undefined. But in your template, you go on to reference
newComment.subcomment.name
So you are essentially trying to get the name property of undefined. There is no name property of undefined. That is why you get your error.
One way you can protect yourself from this error is to check to make sure the subcomment property exists before rendering the element using v-if (forgive me if this is the wrong pug syntax-I'm not that familiar with it)
input#name(type="text")(placeholder="Name")(v-model="newComment.subcomment.name")(v-if="newComment.subcomment")
That will prevent the error.
Alternatively in your editComment method, you could check to see if subcomment exists, and if it doesn't, add it.
if (!comment.subcomment)
comment.subcomment = {name: ''}
this.newComment = comment
Finally, you ask, why does it work if it's not nested data. The key difference is this: if newComment exists, say like this
newComment: {}
and you try to get newComment.name then the returned value of name is undefined. If you have a template that has something like v-model="newComment.name" nothing is going to crash, it's just going to set the value of the input element to undefined, and when you change it, newComment.name will get the updated value.
In other words, when you try to reference newComment.subcomment.name you are trying to reference the name property of something that doesn't exist whereas when you try to reference newComment.name, the object exists, and it doesn't have the name property.
I've got three Polymer-components (Polymer 1.2).
They all sit in their own files therefore is-logged-in and login-name have to be passed from one component to another.
I put them together here so you can understand my problem more easily:
<component1 is-logged-in="true" login-name="Cool Cat">
<component2 is-logged-in="{{isLoggedIn}}" login-name="{{loginName}}">
<component3 is-logged-in="{{isLoggedIn}}" login-name="{{loginName}}"></component3>
</component2>
</component1>
All 3 components have these properties:
properties: {
isLoggedIn: {
type: Boolean,
value: false
},
loginName: {
type: String,
value: ""
}
}
document.querySelector("component2").loginName is Cool Cat but
document.querySelector("component3").loginName is just an empty string.
When checking the DOM is-logged-in and login-name don't appear anymore starting at <component2>
How can I pass the data on to component3 ?
All {{ }} bindings have to live in template, and it's the template which identifies the scope of the values. Other parent-child relationships do not define scope.
In your example, all the component-1/2/3 are in the same template, and therefore in the same scope. Setting properties of component-1 has no effect on component-2 and component-3, they are not bound together.
In other words, the {{isLoggedIn}} and {{loginName}} macros are binding to properties in the scope identified by the containing template (the scope is usually an element, but can also be a dom-repeat or other specialized template).
I don't expect this is actually want you want, but for clarity, something like this would work:
<dom-module id="component-0">
<template>
<component-1 is-logged-in="{{isLoggedIn}}" login-name="{{loginName}}">
<component-2 is-logged-in="{{isLoggedIn}}" login-name="{{loginName}}">
<component-3 is-logged-in="{{isLoggedIn}}" login-name="{{loginName}}"></component-3>
</component-2>
</component-1>
...
<script>
Polymer({
is: 'component-0',
properties {
isLoggedIn: {value: true},
loginName: {value: "cool-cat"}
}
</script>
All the {{ }} bindings are in the component-0 scope, so setting the values in that scope sets the values to all the bindings.
Fwiw, you will probably also have an easier time if you aggregate the shared data into an object.
<component-1 login="{{login}}">
<component-2 login="{{login}}">
<component-3 login="{{login}}"></component-3>
Where e.g. login = {isLoggedIn: true, loginName: "cool-cat"}.
The idea that the data must be passed from one component to another is not true in this construction. If your goal is just to get the data to component-3, you can bind the data directly and ignore 1 and 2.
The only time data must be passed from one component to another is when crossing scopes (a scope defines a boundary for data, so hopefully this make sense).