Accidentally updating child variables - javascript

I'm currently working in a page with parent/child components. Somehow my child component gets updated when I manage its variables in the parent component.
What I'm trying to do:
My child component has a 'site' variable with all the data i need to send via API
My parent component has a Save button to send the child data to the Back-end
When 'site' changes in the child component, I'm emitting an event #change to the parent component
The #change event contains all the data I need, but not in the format I want
There is a function submit() that gets this data and modify the one of the arrays so that this: ['foo','bar'] becomes this 'foo,bar'
The problem when I do the step '5' my child component gets updated
The child component inside the parent component
<configuracoes :configuracoes="configuracoes" #change="setData"
v-if="currentPage === 'configs'"/>
The change event emitted by the child component
this.$emit("change", this.site);
The important part of 'site' var
site: {
seo: {
keywords: [],
...
},
...
},
The setData() function
setData(data) {
this.data = data;
},
The submitData() function
submitData() {
if (this.currentPage === "configs") {
let data = ({}, Object.assign(this.data))
let keywords = data.seo.keywords.join(',')
data.seo.keywords = keywords
this.$store.dispatch("sites/updateSite", {
empresa_id: this.user.empresa_id,
site_id: this.siteId,
dados: data,
});
}
}
As you can see, I'm declaring another variable let data to avoid updating this.site variable, but no success

First of all, there is an issue with how you're "copying" your this.data object.
let data = ({}, Object.assign(this.data)); // this doesn't work
console.log(data === this.data); // true
const dataCopy = Object.assign({}, this.data); // this works
console.log(dataCopy === this.data); // false
The way Object.assign works, all the properties will be copied over into the first argument. Since you only pass a single argument, it doesn't change and is still pointing to the same old object.
If you use the correct way, you will most likely still run into the same issue. The reason is that data.seo is not a primitive value (a number or a string), but is an object.
This means that the whole seo object will be copied over into the new copy. In other words, even though dataCopy !== this.data, dataCopy.seo === this.data.seo. This is known as "shallow copy".
You want to make sure you DO NOT modify the original seo object, here are a few ways to do that.
let goodCopy;
const newKeywords = this.data.seo.keywords.join(',');
// use object spread syntax
goodCopy = {
...this.data,
seo: {
...this.data.seo,
keywords: newKeywords,
},
};
// use Object.assign
goodCopy = Object.assign(
{},
this.data,
{
seo: Object.assign(
{},
this.data.seo,
{keywords: newKeywords}),
});
// create a copy of "seo", and then change it to your liking
const seoCopy = {...this.data.seo};
seoCopy.keywords = newKeywords;
goodCopy = Object.assign({}, this.data, {seo: seoCopy});
this.$store.dispatch('sites/updateSite', {
empresa_id: this.user.empresa_id,
site_id: this.siteId,
dados: goodCopy,
});
If you want to read up on ways to copy a JavaScript object, here's a good question.

Related

Accessing Svelte component properties in a callback?

Imagine that you have a lot of properties in a component:
let a = 'foo';
let b = 'bar';
// ...
let z = 'baz';
You then want to do something like update all of them from an external callback, like in another library (i.e. something that isn't and can't be a Svelte component itself).
A simple use case is just an AJAX method to load in a bunch of data (assume this ajax function works and you can pass it a callback):
onMount(async function() {
ajax('/data', function(data) {
a = data.a;
b = data.b;
// ...
z = data.z;
});
});
This works, but it's incredibly boilerplaty. What I'd really like is a way to loop through all the properties so they can be assigned to programmatically, especially without prior knowledge on the outside library/callback's part.
Is there no way to get access to a Svelte component and its properties so you can loop through them and assign them from an outside function?
Vue has a simple solution to this, because you can pass the component around, and still check and assign to its properties:
var vm = this;
ajax('/data', function(data) {
for (var key in data) {
if (vm.hasOwnProperty(key)) {
vm[key] = data[key];
}
});
});
I have seen some solutions to this, but they're all outdated - none of them work with Svelte 3.
Apologies if this has been asked before. I've spent days trying to figure this out to avoid all that extra boilerplate and the closest I could find is Access Component Object in External Callback? which does not have an answer right now.
If possible, you could put the ajax call in the parent component and have the data returned from it stored in a temporary object, that you then pass on to the component using the spread operator.
<Component { ...dataObject }></Component>
let dataObject = {};
onMount(async function() {
ajax('/data', function(data) {
dataObject = data;
});
});
You can reduce the boilerplate by using destructuring:
onMount(async function() {
ajax('/data', data => {
({ a, b, ..., z } = data);
});
});
But if you have a very large number of variables, you might be better off just putting them in an object in the first place:
let stuff;
onMount(async function() {
ajax('/data', data => {
stuff = data;
});
});

What's the proper way to update props on a Vue component instance

I'm trying to figure out the best way to update propsData created on a component instance. Basically I have a signature wrapper page, and receive a bunch of html which is rendered using v-html. Then I'm creating a variable number of signature pad components within that rendered html. Since I don't know what the html is going to be, I'm forced (best I can tell) to create the components on the fly after mounting.
So I'm running the following on the parent mounted():
initializeSignaturePads() {
const signatureAreas = document.querySelectorAll('.signature_area');
// dynamically create a new vue instance for each signature pad and mount onto the respective .signature_area element
// since the html is loaded via ajax, we don't know where to render this template on load, so a new Vue must be created
signatureAreas.forEach(element => {
const id = element.id;
const signatureType = element.classList.contains('initials') ? 'initials' : 'signature';
if (this.needsCustomerSignature(id)) {
let length = this.signatures.push({
fieldName: id,
valid: false,
data: null,
type: signatureType
});
const SignaturePadClass = Vue.extend(SignaturePad);
const SignaturePadInstance = new SignaturePadClass({
parent: this,
propsData: {
fieldName: id,
editable: true,
signatureType: signatureType,
signatureIndex: length - 1,
signatureData: null
}
});
// add handler for signed emit
SignaturePadInstance.$on('signed', signature => {
this.padSigned(signature);
});
// watch this for an accepted signature, then pass to each child
this.$watch('createdSignature.accepted', function (val) {
let signatureData = null;
if (val) {
signatureData = signatureType == 'signature' ? this.createdSignature.signatureData : this.createdSignature.initialsData;
}
// These two lines are the problem
SignaturePadInstance._props.signatureData = signatureData;
SignaturePadInstance._props.editable = !val;
});
SignaturePadInstance.$mount(element);
}
});
},
As far as I can tell, that propsData is now statically set on the component. But for the signatureData and editable props, I need to be able to pass that to the child components when they're updated. The watcher is working correctly and the prop is getting updated, but I'm getting the Avoid mutating a prop directly warning. Which is understandable, since I'm directly mutating the prop on the child. Is there Is there a good way to handle this?
I was able to get this figured out, after I found this stackoverflow answer. When setting props on the propsData, I was using all primitive types, so they didn't have the built in reactive getters and setters. It makes sense now that I realize that, what I was doing was the equivalent of passing a string as a prop to an component element. After I did that, the prop was reactive and I didn't have to bother with manually creating watchers.
Anyways, this was the solution:
const SignaturePadInstance = new SignaturePadClass({
parent: this,
propsData: {
fieldName: id, // << primitive
editable: true, // << primitive
signatureType: signatureType, // << primitive
signatureIndex: length - 1, // << primitive
createdSignature: this.createdSignature // << reactive object, updates to the child when changed
}
});
I used Vue.observable (VueJS 2.6 and above) to make the property reactive. Here's a complete example:
initializeSignaturePad() {
const signaturePadComponent = Vue.extend(SignaturePad)
this.instance = new signaturePadComponent()
instance._props = Vue.observable({
...instance._props,
editable: true
})
this.instance.$mount()
document.body.appendChild(instance.$el)
}
onSignatureAccepted {
this.instance.editable = false
}

How to pass by value and not by reference in React?

I have the following file, LookupPage.jsx and AccountDetails.jsx.
In LookUp
this.updateCustomer = (customer) => {
if(JSON.stringify(customer.address) !== JSON.stringify(this.state.activeAccount.customer.address)) {
console.log('address changed');
customer.update_address = true;
customer.address.source = 'user';
}
return fetch(
`${API_ENDPOINT}/customer/${customer.id}/`,
{
method: 'PATCH',
headers: {
'Authorization': 'Token ' + this.props.session_token,
'Content-Type': 'application/json',
},
body: JSON.stringify(customer),
}
).then(restJSONResponseToPromise).then(responseJSON => {
if(responseJSON.results){
console.log('update customers client side.')
}
}, clearSessionIfInvalidToken(this.props.clearSession));
};
<AccountsDetailModal
show={this.state.showAccountDetail}
close={this.toggleAccountDetail}
customer={this.state.activeAccount.customer}
updateCustomer={this.updateCustomer}
/>
In side AccountDetails
this.onChangeAddress = (e) => {
const customer = {...this.state.customer};
const address = customer.address;
address[e.target.name] = e.target.value;
customer.address = address;
this.setState({customer, errors: {
...this.state.errors,
[e.target.name]: [],
}});
};
this.saveCustomer = () => {
this.setState({postDisable: true});
const errors = this.getFormErrors();
const hasErrors = !every(errors, (item) => !item.length);
if(!hasErrors){
this.props.updateCustomer(this.state.customer);
} else {
sweetAlert('Error!', 'Form is invalid.', 'error');
}
this.setState({postDisable: false});
};
this.componentDidMount = () => {
this.setState({customer: this.props.customer});
}
When I am updating the customers address, it is updating active accounts address, so it seems like it is being passed by reference. What I want to happen is only update the customer address if the address was changed/different from the original. How would I modify my code to do this?
You can pass any object by value in JS (whether you're using React or not) by passing:
JSON.parse(JSON.stringify(myObject))
as an argument instead of the object itself.
Essentially this will just clone the object and pass a copy of it, so you can manipulate the copy all you want without affecting the original.
Note that this will not work if the object contains functions, it will only copy the properties. (In your example this should be fine.)
I am going to put my two cents here:
First of all, this isn't really specific to React and is more of a JS related question.
Secondly, setting props against internal state is considered to be a bad practice when it comes to react. There's really no need to do that given your particular scenario. I am referring to
this.setState({customer: this.props.customer});
So, coming to your problem, the reason you are having reference issues is because you are mutating the original passed in object at certain points in your code. For instance, if I look at:
this.updateCustomer = (customer) => {
if(JSON.stringify(customer.address) !== JSON.stringify(this.state.activeAccount.customer.address)) {
console.log('address changed');
customer.update_address = true;
customer.address.source = 'user';
}
};
You are mutating the original props of the argument object which is very likely to be passed around in other methods of your component. So, to overcome that you can do:
const updatedCustomer = Object.assign({}, customer, {
update_address: true
});
And you can pass in updatedCustomer in your API call. Object.assign() will not perform operation on the passed in object but will return a new object so you can be sure that at any point in your app you are not mutating the original object.
Note: Object.assign would work on plain object and not a nested one. So, if you want to achieve something similar that would work on nested object properties too, you can use lodash merge.

Pass properties from parent component to all transcluded children component in Vue

I would like to pass some properties from a parent to all of his children when those are transcluded (content distribution syntax). In this case, the parent doesen't know (as far as I know) his children, so I don't know how to proceed.
More specificly, I want a way to write this :
<my-parent prop1="foo" prop2="bar">
<my-children></my-children> <!-- Must know content of prop1 and prop2 -->
<my-children></my-children> <!-- Must know content of prop1 and prop2 -->
</my-parent>
Instead of having to write this :
<my-parent prop1="foo" prop2="bar">
<my-children prop1="foo" prop2="bar"></my-children>
<my-children prop1="foo" prop2="bar"></my-children>
</my-parent>
Is it possible ? Thanks.
Props allow data flow only one level. If you want to perpetuate data, you can use an event bus instead.
Instantiate an event bus with an empty Vue instance in your main file.
var bus = new Vue();
Then in your parent, emit the event with data to be passed
bus.$emit('myEvent', dataToBePassed);
Listen for myEventanywhere you want to pick up the data. In your case, it is done in your child components
bus.$on('myEvent', function(data) {
.....
});
Here is my solution, that's probably not a great deal, but that's the cleanest solution for what I want to do right now. The principle is to create computed properties that will use own component prop if they exist, or get $parent values otherwise. The real prop would then be accessible in this._prop.
Vue.component('my-children', {
props: ["prop1", "prop2"],
template: "<div>{{_prop1}} - {{_prop2}}</div>",
computed: {
_prop1: function() {
return this.prop1 || this.$parent.prop1;
},
_prop2: function() {
return this.prop2 || this.$parent.prop2;
}
}
});
Here is a mixin generator that does that in a more elegant way, and with, possibly, multiple levels :
function passDown(...passDownProperties) {
const computed = {};
passDownProperties.forEach((prop) => {
computed["_" + prop] = function() {
return this[prop] || this.$parent[prop] || this.$parent["_" + prop];
};
});
return { computed };
}
Vue.component('my-children', {
props: ["prop1", "prop2"],
template: "<div>{{_prop1}} - {{_prop2}}</div>",
mixins: [passDown("prop1", "prop2")]
});
At this point (I'm not a vue expert) I just could think in this solution.
Assign every component's props is boring I agree, so why not doing it programmatically?
// Create a global mixin
Vue.mixin({
mounted() { // each component will execute this function after mounted
if (!this.$children) {
return;
}
for (const child of this.$children) { // iterate each child component
if (child.$options._propKeys) {
for (const propKey of child.$options._propKeys) { // iterate each child's props
// if current component has a property named equal to child prop key
if (Object.prototype.hasOwnProperty.call(this, propKey)) {
// update child prop value
this.$set(child, propKey, this[propKey]);
// create a watch to update value again every time that parent property changes
this.$watch(propKey, (newValue) => {
this.$set(child, propKey, newValue);
});
}
}
}
}
},
});
This works but you will get an ugly vue warn message:
[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value.
I'm not sure if this is a good solution but it works, so if you decide to use just keep in mind Global-Mixin recomendations:
Use global mixins sparsely and carefully, because it affects every
single Vue instance created, including third party components.
Please see a full example at https://github.com/aldoromo88/PropsConvention
Hope it helps

How can I store functions within the HTML5 history states

So I'm using the HTML5 history management for adding the ability to navigate back and forward within a website with AJAX loaded subcontent.
Now I would like to store javascript functions within the state object, to callback at the state popping. More or less like the following code:
$(window).bind("popstate", function(event) {
var state = event.originalEvent.state;
if (!state) {
return;
}
state.callback(state.argument);
}
function beforeLoad() {
var resourceId = "xyz";
var func;
if (case1) {
func = switchPageToMode1;
} else { // case 2
func = swithPageToMode2;
}
func(resourceId); // run the function
window.history.pushState({ callback: func, resourceId: resourceId }, "newTitle", "newURL"); // and push it to history
}
function switchPageToMode1(resourceId) {
alterPageLayoutSomeWay();
loadResource(resourceId);
}
function swithPageToMode2(resourceId) {
alterPageLayoutSomeOtherWay();
loadResource(resourceId);
}
function loadResource(resourceId) {
...
}
All right. So what I'm trying to do is storing a reference to a javascript function. But when pushing the state (the actual window.history.pushState call) the browser files a complaint, namely Error: "DATA_CLONE_ERR: DOM Exception 25"
Anybody knows what I'm doing wrong? Is it at all possible to store function calls within the state?
No, it's not possible, not directly anyway. According to MDC the "state object," i.e. the first argument to pushState, "can be anything that can be serialized." Unfortunately, you can't serialize a function. The WHATWG spec says basically the same thing but in many more words, the gist of which is that functions are explicitly disallowed in the state object.
The solution would be to store either a string you can eval or the name of the function in the state object, e.g.:
$(window).bind("popstate", function(event) {
var state = event.originalEvent.state;
if ( !state ) { return; }
window[ state.callback ]( state.argument ); // <-- look here
}
function beforeLoad() {
var resourceId = "xyz",
func
;
if ( case1 ) {
func = "switchPageToMode1"; // <-- string, not function
} else {
// case 2
func = "swithPageToMode2";
}
window[ func ]( resourceId ); // <-- same here
window.history.pushState(
{ callback : func,
argument : resourceId
},
"newTitle", "newURL"
);
}
Of course that's assuming switchPageToMode1 and -2 are in the global context (i.e. window), which isn't the best practice. If not they'll have to be accessible somehow from the global context, e.g. [window.]MyAppGlobal.switchPageToMode1, in which case you would call MyAppGlobal[ func ]( argument ).
I came up with a slightly different solution.
I added two variables to the window variable
window.history.uniqueStateId = 0;
window.history.data = {}.
Each time I perform a pushstate, all I do is push a unique id for the first parameter
var data = { /* non-serializable data */ };
window.history.pushState({stateId : uniqueStateId}, '', url);
window.history.data[uniqueStateId] = data;
On the popstate event, I then just grab the id from the state object and look it up from the data object.
Here is what I do:
Each HTML page contains one or more components that can create new History entries.
Each component implements three methods:
getId() which returns its unique DOM id.
getState() that returns the component's state:
{
id: getId(),
state: componentSpecificState
}
setState(state) that updates the component's state using the aforementioned value.
On page load, I initialize a mapping from component id to the component like so:
this.idToComponent[this.loginForm.getId()] = this.loginForm;
this.idToComponent[this.signupForm.getId()] = this.signupForm;
Components save their state before creating new History entries:
history.replaceState(this.getState(), title, href);
When the popstate event is fired I invoke:
var component = this.idToComponent[history.state.id];
component.setState(history.state);
To summarize: instead of serializing a function() we serialize the component id and fire its setState() function. This approach survives page loads.

Categories