Ember component computed function does not rerun when data changes - javascript

I have stored a string value within a computed property like so:
clientId: Ember.computed.oneWay("_clientId")
where _clientId is defined as a property on the object like so:
export default Ember.service.extend { _clientId: null, clientId: Ember.computed.oneWay("_clientId"), updateId() {this.set("_clientId", "fe48822d-bf50-44a1-9ce0-61b06504d726"); } }
I then have a component with a computed property like so:
chartData: Ember.computed("data", function () {
const data = this.get("data");
const clientId = this.get("session").get("clientId");
console.log(clientId);
if (data && data.get("left") !== undefined && data.get("right") !== undefined) {
return data;
}
this.set("_chartDisplayData", null);
return null;
})
When I called updateId, i expected the chartData function to be re-run as the value of the clientId is changed (i verified that the value gets changed for clientId). However, the chartData function never re-runs, why is this?

You need to tell the computed property about all of your dependencies. First, the computed property will never run if it isn't being used somewhere. If you aren't using it you need an observer instead. But assuming you are actually using it, the computed property will only recompute itself when one of the listed dependencies change. And if you list an object as a dependency, it will not update if only some of the object's properties are changed, only if the entire object is replaced. Try this:
chartData: Ember.computed("data.left", "data.right", "session.clientId", function () {
const data = this.get("data");
const clientId = this.get("session.clientId");
console.log(clientId);
if (data && data.get("left") !== undefined && data.get("right") !== undefined) {
return data;
}
this.set("_chartDisplayData", null);
return null;
})

Related

custom function changes Vue.js data value as side effect

I have a function inside the methods property which takes passedData value from the data() method, does some changes to the object and stores the new value in a constant
this somehow causes a side effect which changes the passedData value also.
What is the cause and how can I prevent that ?
this.passedData: -->
{"propss":"value"}
App.vue?3dfd:61 {"propss":"propss : value"}
App.vue?3dfd:49 {"propss":"propss : value"}
App.vue?3dfd:61 {"propss":"propss : propss : value"}
new Vue({
el: "#app",
data() {
return {
passedData: { propss: "value" },
};
},
methods: {
displayData() {
console.log(JSON.stringify(this.passedData));
const root = d3.hierarchy(this.passedData, function(d) {
if(typeof d == "object")
return Object.keys(d).filter(d=>d!="$name").map(k=>{
if(typeof d[k] == "object") d[k].$name = k;
else d[k] = k + " : " + d[k];
return d[k];
});
})
console.log(JSON.stringify(this.passedData));
},
},
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<button type="button" #click="displayData">display</button>
</div>
The problem you are facing here is, that data in vue.js is reactive. So changing data stored anywhere will change the source of the data too.
If you want to change something without facing issues due to reactivity, you need to assign this data without reference as you need. Small example:
// Passing your reactive data
passedData: { propss: "value" },
newData: {}
// Passing data to new property and reactivity
this.newData = this.passedData
this.newData.propss = 'newValue' // this will change passedData.propss to "newValue" too
// Assign Data without reference
this.newData = JSON.parse(JSON.stringify(this.passedData))
If you now change newData it wont affect passedData, as JSON.parse(JSON.stringify(this.passedData)) created a new state without reference to the original.
Note that this should only be a workaround, it isn´t a proper state management.

Why isn't the mobx #computed value?

Simple: the computed value isn't updating when the observable it references changes.
import {observable,computed,action} from 'mobx';
export default class anObject {
// THESE WRITTEN CHARACTERISTICS ARE MANDATORY
#observable attributes = {}; // {attribute : [values]}
#observable attributeOrder = {}; // {attribute: order-index}
#observable attributeToggle = {}; // {attribute : bool}
#computed get orderedAttributeKeys() {
const orderedAttributeKeys = [];
Object.entries(this.attributeOrder).forEach(
([attrName, index]) => orderedAttributeKeys[index] = attrName
);
return orderedAttributeKeys;
};
changeAttribute = (existingAttr, newAttr) => {
this.attributes[newAttr] = this.attributes[existingAttr].slice(0);
delete this.attributes[existingAttr];
this.attributeOrder[newAttr] = this.attributeOrder[existingAttr];
delete this.attributeOrder[existingAttr];
this.attributeToggle[newAttr] = this.attributeToggle[existingAttr];
delete this.attributeToggle[existingAttr];
console.log(this.orderedAttributeKeys)
};
}
After calling changeAttribute, this.orderedAttributeKeys does not return a new value. The node appears unchanged.
However, if I remove the #computed and make it a normal (non-getter) function, then for some reason this.orderedAttributeKeys does display the new values. Why is this?
EDIT: ADDED MORE INFORMATION
It updates judging by logs and debugging tools, but doesn't render on the screen (the below component has this code, but does NOT re-render). Why?
{/* ATTRIBUTES */}
<div>
<h5>Attributes</h5>
{
this.props.appStore.repo.canvas.pointerToObjectAboveInCanvas.orderedAttributeKeys.map((attr) => { return <Attribute node={node} attribute={attr} key={attr}/>})
}
</div>
pointerToObjectAboveInCanvas is a variable. It's been set to point to the object above.
The changeAttribute function in anObject is called in this pattern. It starts in the Attribute component with this method
handleAttrKeyChange = async (existingKey, newKey) => {
await this.canvas.updateNodeAttrKey(this.props.node, existingKey, newKey);
this.setState({attributeEdit: false}); // The Attribute component re-renders (we change from an Input holding the attribute prop, to a div. But the above component which calls Attribute doesn't re-render, so the attribute prop is the same
};
which calls this method in another object (this.canvas)
updateNodeAttrKey = (node, existingKey, newKey) => {
if (existingKey === newKey) { return { success: true } }
else if (newKey === "") { return { success: false, errors: [{msg: "If you'd like to delete this attribute, click on the red cross to the right!"}] } }
node.changeAttribute(existingKey, newKey);
return { success: true }
};
Why isn't the component that holds Attribute re-rendering? It's calling orderedAttributeKeys!!! Or am I asking the wrong question, and something else is the issue...
An interesting fact is this same set of calls happens for changing the attributeValue (attribute is the key in anObject's observable dictionary, attributeValue is the value), BUT it shows up (because the Attribute component re-renders and it pulls directly from the node's attribute dictionary to extract the values. Again, this is the issue, an attribute key changes but the component outside it doesn't re-render so the attribute prop doesn't change?!!!
It is because you have decorated changeAttribute with the #action decorator.
This means that all observable mutations within that function occur in a single transaction - e.g. after the console log.
If you remove the #action decorator you should see that those observables get updated on the line they are called and your console log should be as you expect it.
Further reading:
https://mobx.js.org/refguide/action.html
https://mobx.js.org/refguide/transaction.html
Try to simplify your code:
#computed
get orderedAttributeKeys() {
const orderedAttributeKeys = [];
Object.entries(this.attributeOrder).forEach(
([attrName, index]) => orderedAttributeKeys[index] = this.attributes[attrName])
);
return orderedAttributeKeys;
};
#action.bound
changeAttribute(existingAttr, newAttr) {
// ...
};
Also rename your Store name, Object is reserved export default class StoreName

Copying React State object; modifying copied object changes state object

I am trying to copy a state object:
#boundMethod
private _onClickDeleteAttachment(attachmentName: string): void {
console.log("_onClickDeleteAttachment | this.state.requestApproval[strings.Attachments]: ", this.state.requestApproval[strings.Attachments]);
let requestApprovalClone = {... this.state.requestApproval}
if (requestApprovalClone === this.state.requestApproval) {
console.log("they are ===");
}
else {
console.log(" they are not ===");
}
_.remove(requestApprovalClone[strings.Attachments], (attachment: any) => {
return attachment.FileName === attachmentName;
})
console.log("_onClickDeleteAttachment | this.state.requestApproval[strings.Attachments]: ", this.state.requestApproval[strings.Attachments]);
console.log("_onClickDeleteAttachment | requestApprovalClone[strings.Attachments]: ", requestApprovalClone[strings.Attachments]);
}
The state object is being altered too. From what I have read, I shouldn't mutate a state object but only change it with setState.
How can I correct this?
You are getting that behavior, because the
let requestApprovalClone = {... this.state.requestApproval}
is only shallow copying the data, your attachments property has some nested objects and it keeps the same reference and therefore when changing it, the cloned object gets altered and the state too.
To avoid that, you can perform another copy of your attachments property like this :
let attachments = [...requestApprovalClone[strings.Attachments]];
_.remove(attachments, function (attachment) {
return attachment.FileName === attachmentName;
});
Changing the attachments variable content won't afftect the state anymore.
you can read more about that behavior here
It has to to with the way that JS handles their const references.
For those who feel adventerous:
let requestApprovalClone = JSON.parse(JSON.stringify(this.state.requestApproval));
// modify requestApprovalClone ...
this.setState({
requestApproval:requestApprovalClone
})
It would be interesting if Object.assign({},this.state.requestApproval); is faster than the whole JSON stringify/parse stuff or vice versa
You can have something like:
let requestApprovalClone = Object.assign({},this.state.requestApproval);
requestApprovalClone.strings.Attachments = requestApprovalClone.strings.Attachments.slice(); // will create a shallow copy of this array as well
_.remove(requestApprovalClone[strings.Attachments], (attachment: any) => {
return attachment.FileName === attachmentName;
})
this.setState({
requestApproval:requestApprovalClone
})// If you want to update that in state back

VueJS $set with variable keypath

I'm currently working on a simple filemanager component which I trigger from parent component. After selecting media in the filemanager I $dispatch a simple data object with 2 keys: element & media. I use element to keep track where I want the media to be appended to my current data object and media has the media information (id, type, name and so on). This setup gives me some trouble when I want to $set the media data to variables within my data object. The variables are locales, so: nl-NL, de-NL and so on.
setMediaForPage : function(data){
if(!this.page.media[this.selectedLanguage]['id'])
{
// set for all locales
var obj = this;
this.application.locales.forEach(function(element, index, array) {
obj.$set(obj.page.media[element.locale], data.media);
})
}
else
{
// set for 1 locale
this.$set(this.page.media[this.selectedLanguage], data.media);
}
}
What happens when I run this code is that the data object shows up properly in Vue Devtools data object, but the media does not show up in the template. When I switch the language (by changing the this.selectedLanguage value), the media does show up.
I think this has to do with the variables in the object keypath, but I'm not 100% sure about that. Any thoughts on how to improve this code so I can show the selected media in the parent component without having to change the this.selectedLanguagevalue?
I don't know your data structure exactly, but you can certainly use variables as the the keypath in vue, however remember that the keyPath should be a string, not an object.
If your variable that you want to use in the keypath is part of the vue, you'd do it like this:
obj.$set('page.media[element.locale]', data.media)
... because the keyPath which is a string is intelligently parsed by Vue's $set method and is of course it knows that this path is relative to the $data object.
new Vue({
el: '#app',
data() {
return {
msg: "hello world",
attr: {
lang: {
zh: '中文',
en: 'english'
}
}
}
},
methods: {
$set2(obj, propertyName, value) {
let arr = propertyName.split('.');
let keyPath = arr.slice(0, -1).join('.');
let key = arr[arr.length - 1];
const bailRE = /[^\w.$]/
function parsePath(obj, path) {
if (bailRE.test(path)) {
return
}
const segments = path.split('.')
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
let target = parsePath(obj, keyPath);
// console.log(target, key);
// target[key] = value;
this.$set(target, key, value);
}
},
mounted() {
setTimeout(() => {
// this.$set('attr.lang.zh', '嗯');
// this.$set2(this, 'attr.lang.zh', '嗯');
this.$set2(this.attr, 'lang.zh', '嗯');
}, 1000);
}
})
调用示例:this.$set2(this.attr, 'lang.zh', '嗯');
i have also experienced similar problems,remove variables -,these variables nl-NL, de-NL change to nlNl, deNl
and i not use
obj.$set('page.media[element.locale]', data.media)
but
obj.$set('page.media.'+element.locale, data.media);
then it work

KnockoutJS - computed abservable and js object

I am trying to return a property of an observable but seem to be missing something.
self.SelectedAccountTypeID = ko.computed(function () {
return self.selectedAccountType.AccountTypeID();
});
I am trying to return the AccountTypeID property of selectedAccountType but this is not working
when I try
self.SelectedAccountTypeID = ko.computed(function () {
return self.selectedAccountType();
});
it works but returns a javascript object
Here is a fiddle with the code
http://jsfiddle.net/qafrD/
You are on the right track, because your selectedAccountType is an observable you need to access its value with selectedAccountType()
So the correct syntax: self.selectedAccountType().AccountTypeID;
However because the self.selectedAccountType() can be null you need to check that first before accessing the AccountTypeID on it:
self.SelectedAccountTypeID = ko.computed(function () {
if (self.selectedAccountType())
return self.selectedAccountType().AccountTypeID;
});
Demo Fiddle
This is because self.selectedAccountType is an observable meaning that you need to invoke it like a function to retrieve its current value. The property "AccountTypeID" however is not an observable therefore you do not need parenthesis here.
self.SelectedAccountTypeID = ko.computed(function () {
// Retrieve the value of the observable
var selectedAccountType = self.selectedAccountType();
// The value may be "undefined" or "null" if there has not yet been
// anything stored in the observable
if (selectedAccountType && typeof selectedAccountType.AccountTypeID != "undefined") {
return selectedAccountType.AccountTypeID;
}
// Return a default value otherwise
return null;
});
Demo: http://jsfiddle.net/qafrD/1/

Categories

Resources