How to avoid duplication between mounting and updating a Vue component - javascript

In a Vue application I'm working on I have a number of form components that can be used to either create a new record or amend an existing one. While a form is open, it is also possible to click another record or click create, in which case the contents of the form are replaced or cleared respectively.
The problem I have is that I can't seem to avoid a lot of duplication between my data function and my watch functions.
Here's a simplified example of the kind of thing I mean:
props: ["record"],
data() {
return {
name: this.record ? this.record.name : "",
age: this.record ? this.record.age : null
};
},
watch: {
record(record) {
this.name = record ? record.name : "";
this.age = record ? record.age : null;
}
}
Everything I have to do to set up the form when it's mounted has to be done twice: once in the data function to set up the initial reactive properties, and then again in watch for any props that could change. This gets more and more difficult to manage and mistake-prone as the number of properties in the record gets bigger.
Is there any way to keep this setup logic in one place and avoid this duplication?

To resolve the problem add a immediate property to your watcher which will make it call at initialization too. Therefore the initial value of your record property will be handled. Take a look at the code below:
props: ["record"],
data() {
return {
name: "",
age: null
};
},
watch: {
record: {
immediate: true,
handler(value) {
this.name = this.record ? this.record.name : "";
this.age = this.record ? this.record.age : null;
}
}
}
Reference: vm.$watch - Vue's Official API

How about this?
props: ["record"],
data() {
return this.updateRecord(this.record, {});
},
watch: {
record(record) {
this.updateRecord(record, this);
}
},
updateRecord(what, where) {
where.name = what ? what.name : "";
where.age = what ? what.age : null;
return where;
}

Related

Vue3 reactivity weird behavior

I am learning vue3 reactivity system and cannot figure out the following strange behavior. Here's my code.
setup(props) {
let weekDay = props.weekDay;
let taskState = reactive({
[weekDay]: [[{
description: '',
status: '',
}]],
});
function changeStatus(index, event) {
let task = taskState[weekDay][index]; //works
taskState[weekDay][index] = {
...task,
status: event.target.value
};
}
return {
taskState,
changeStatus
}
}
updating the state status reactively looks like works, but if i change the function like below, this would not work. Why is it happening? are object task and taskState[weekDay][index] not
same ?
function changeStatus(index, event) {
let task = taskState[weekDay][index]; //not works
task = {
...task,
status: event.target.value
};
}
It seems that task object is not a reactive, because we change the value using an assignment to a regular object, and the object task no longer has a reactive parent such as taskState, so there is no setter attached to it and a simple value is returned, instead of a reactive proxy
function changeStatus(index, event) {
let task = taskState[weekDay][index]; //not works
task = {
...task,
status: event.target.value
};
}
if we want to make it work we should use like this:
function changeStatus(index, event) {
let task = taskState[weekDay]; //works
task[index] = {
...task[index],
status: event.target.value
};
}
in that case task[index] will be reactive and the setter will be triggered in the taskState[weekDay] object

Vue Computed Property not updating. Very strange behaviour

Yes, it's another 'Vue computed property is not updating question...
Below is an excerpt of my component with the issue. I have a computed property 'fieldModel' this uses Vue.set to set a new value, then i console log that computed property immediately after assigning it a new value the javascript object updates and is viewable in devtools, the computed property however has not updated, and neither has the DOM.
export default {
props:{
value:{
type:Object,
required:true,
}
},
data() {
return {
model:this.value,
key:'something',
}
},
created() {
var self = this;
setTimeout(function() {
self.fieldModel = 'Apples';
}, 1000);
},
computed:{
fieldModel:{
get() {
return this.model[this.key];
},
set(value) {
var self = this;
self.$set(self.model, self.key, value);
console.log(self.model[self.key], self.fieldModel);
//Logs out 'Apples', undefined,
}
}
}
}
The example code i posted in the original question works correctly, This lead me to break down my code and resolve my issue.
I had this component in a v-for loop with recursive nesting, Another component appeared to mutate the v-model object without updating these components resulting in some very strange behaviour.
I was able to solve the problem by adding a watcher for 'value' to update the model field and a watcher for 'model' to $emit('input') any changes to the model to it's parent.
This results in an infinite loop that crashes the browser, however i was able to resolve that by adding a check to see if the model/value is the same object
Example code is simplified for brevity:
{
watch:{
value(newValue) {
if(this.model != newValue) {
this.model = newValue;
}
},
model(newModel) {
this.$emit('input', newModel)
},
}
}

Why I shouldn't set data from computed?

Using vuex, I receive an object, lets suppose
user: {name: 'test'}
And in app.vue I use this.$store.getters.user
computed: {
user: function() {
let user = this.$store.getters.user
return user
}
}
While setting also data object 'this.name'
data() {
return {
name: ''
}
}
computed: {
user: function() {
let user = this.$store.getters.user
this.name = user.name
return user
}
}
But in the lint I get this error 'unexpected side effect in computed property', (the data 'name' should be used as a v-model, to be used as a update API parameter).
I know it can be ignored if you know what you're doing, and that it is triggered for setting data from computed, but why it triggers this error? and how to workaround it?
don't set value in computed. if you need to get name of computed user you must be create new computed:
user: function() {
let user = this.$store.getters.user
return user
},
name: function() {
if(this.user.name!=undefined) return this.user.name
return ''
},
and remove name from data
but if you realy need to set name you can watch user and set name
watch: {
user(newVal) {
if(newVal.name!=undefined) this.name = newVal.name
}
}
Vue has both computed getters and setters. If you define a computed property as you did above it is only a getter. A getter is meant to only "get" a value and it should be a "pure" function with no side effects so that it is easier to read, debug and test.
From the docs for the rule that triggered the linting error on your code:
It is considered a very bad practice to introduce side effects inside
computed properties. It makes the code not predictable and hard to
understand.
In your case you might want to use a computed setter for either the user or the name values so that you can use them as v-models. You could, for example, do:
computed: {
user: function () {
return this.$store.getters.user;
},
user: {
// getter
get: function () {
return this.user.name;
},
// setter
set: function (newValue) {
this.$store.commit('setUserName', newValue);
}
}
}

How to make reusable function which affect different data in angular?

First of all, I am so sorry if the title is not represented the problem I am about tell. Here, I have a lot of component which has an object that quite do the same thing. I'll give two example:
First component, PlanningComponent:
export class PlanningComponent implements OnInit {
constructor() {}
data = {
items: null,
isEmpty: null as boolean,
getData: () => {
/* fetching data from database and store it in items properties */
},
/* another method related to data goes here */
};
pagination = {
totalData: null as number, /* this prop gets its value within getData process */
currentPage: null as number, /* this prop already has a value within OnInit process */
goNext: () => {
this.pagination.currentPage += 1;
this.data.getData();
},
goLast: () => {
this.pagination.currentPage = this.totalData;
this.data.getData();
},
/* another method related to pagination goes here */
};
filter = {
filterA: null as string,
filterB: null as number,
filterC: null as string,
isEnabled: null as boolean,
isToggled: null as boolean,
onReset: () => {
this.filter.filterA = null;
this.filter.filterB = null;
this.filter.filterC = null;
this.data.getData();
},
/* another function related to filter goes here */
};
}
Second component, HarvestComponent:
export class HarvestComponent implements OnInit {
constructor() {}
data = {
items: null,
isEmpty: null as boolean,
getData: () => {
/* fetching data from database and store it in items properties */
},
/* another method related to data goes here */
};
pagination = {
totalData: null as number, /* this prop gets its value within getData process */
currentPage: null as number, /* this prop already has a value within OnInit process */
goNext: () => {
this.pagination.currentPage += 1;
this.data.getData();
},
goLast: () => {
this.pagination.currentPage = this.totalData;
this.data.getData();
},
/* another method related to pagination goes here */
};
filter = {
filterX: null as string,
filterY: null as number,
filterZ: null as string,
isEnabled: null as boolean,
isToggled: null as boolean,
onReset: () => {
this.filter.filterX = null;
this.filter.filterY = null;
this.filter.filterZ = null;
this.data.getData();
},
/* another function related to filter goes here */
};
}
Here as you can see, the two component is looks quite the same. The difference lies on the value of data.items, the filter properties, and the affected data when calling the function (for example pagination.goNext()). In the first component is calling the getData() of planning, and the second one is calling the getData() of harvest. Yeah you got the point.
I don't want to write the same code again and again, the apps I am about to develope has a lot of pages but has a similar behaviour. Is there any good approach to make the code reusable in my case?
I have tried to think about create different component for pagination and filter, but I still don't get a clear picture how to make the component affecting the different data (data.items() in planning, data.items() in harvest)
So far, I just create a helper function. For example for filter component I created a function in helper to make props within filter become null. But still, i write the same thing in every component, it just cut a line or two. Is there any hint for me?
As people suggested, you may use pipes.
But I think you have forgotten the main benefit of OOP (which can't be used in Javascript, but is implemented in Typescript). And it's class inheritance! :)
Remember the basics:
class base {
variable1;
constructor() {
this.variable1 = 1;
}
}
class child extends base {
variable2;
constructor() {
super();
console.log(this.variable1);
}
}
And the same is for class members as methods and properties.
Thanks to Typescript, it is possible now.

Using conditional logic in computed properties fails to update

I have two fiddles: A, B (using Vuejs 2.2.4)
I have a computed property which can be changed programmatically (I am using the get and set methods).
Expectations:
If the default parameter changes (this.message), the computed property (computedMessage) must change (default behaviour).
If the secondary parameter changes (this.messageProxy), only then the computed property must reflect the secondary parameter.
Fiddle A works as expected but Fiddle B doesn't.
Error: The default behaviour (point 1) stops after the secondary parameter changes.
The only difference between the fiddles is a console statement in the computed property.
Background: I was trying to set a computed property programatically. The computed property is set like:
computedMessage: {
get () {
let messageProxy = this.messageProxy
this.messageProxy = null
console.log(messageProxy, this.messageProxy, this.message)
return messageProxy || this.message
},
set (val) {
this.messageProxy = val
}
}
This allows me to set the value of computedMessage like:
this.computedMessage = 'some string'
If these lines:
get () {
let messageProxy = this.messageProxy
this.messageProxy = null
return messageProxy || this.message
}
were to be replaced with:
get () {
return this.messageProxy || this.message
}
then computedMessage can no longer get access to this.message the moment this.messageProxy is set.
By setting this.messageProxy to null I ensure that the
computedMessage = this.messageProxy
only if an assignment is made.
The reference to this.message in the return statement isn't triggering computedMessage to update. This is because its location in the logical || statement makes it inaccessible. It's a gotcha documented in the Vue.js Computed Properties Documentation.
From the Docs:
status: function () {
return this.validated
? this.okMsg
: this.errMsg // errMsg isn't accessible; won't trigger updates to status
}
The workaround is to explicitly access dependencies:
status: function () {
// access dependencies explicitly
this.okMsg
this.errMsg
return this.validated
? this.okMsg
: this.errMsg
}
So in your example add a reference to this.message:
get() {
this.message
let messageProxy = this.messageProxy
this.messageProxy = null
return messageProxy || this.message
}
The reason your first fiddle was working as expected was because the console.log call had this.message as a parameter.
The actual problem with your code is that you are changing data values in your get function, and they are data values that trigger the re-computation of the get function. Don't do that. The get should just be computing a value based on other values. In this case, it should be
get () {
console.log(this.messageProxy, this.message);
return this.messageProxy || this.message;
},
With or without the console message, it will do what it is supposed to do.
Having re-checked your expectations, I see that you want the override to be cleared whenever the default message changes. You can do that with an additional watch:
var demo = new Vue({
el: '#demo',
data() {
return {
message: 'I am a great guy',
messageProxy: null,
someText: ''
}
},
computed: {
computedMessage: {
get() {
return this.messageProxy || this.message
},
set(val) {
this.messageProxy = val
}
}
},
methods: {
overrideComputed() {
this.computedMessage = this.someText
}
},
watch: {
message: function() {
this.messageProxy = null;
}
}
})
div {
margin: 5px;
}
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.2.4/vue.min.js"></script>
<div id="demo">
<p>This message must reflect value of input1</p>
<div>
{{ computedMessage }}
</div>
input1: <input type="text" v-model='message'>
<div>
<p>This will cause computed message to reflect input2</p>
input2: <input type="text" v-model='someText'>
<button #click='overrideComputed'>Override</button>
</div>
</div>
PS: You don't really need a settable computed here. You could have overrideComputed set messageProxy directly.

Categories

Resources