In my Vue.js app, I have an array value that should only be updated when a user completes a specific "refresh" action. However, as soon as I assign a new value to that array value, the array value becomes reactive and changes instantly as the data in the assigned value changes. The array value should remain un-reactive.
For example, I have a method, refresh(), which when triggered is meant to update displayedData, which should not be reactive, with currentData, which should be reactive. displayedData should only update when refresh is called.
methods: {
refresh: function() {
this.displayedData = this.currentData;
}
}
To make a value not reactive without making it static, you can make a "deep copy" of it using stucturedClone(), which is becoming widely supported as of 2022:
this.displayedData = structuredClone(this.currentData);
Another widely used method is to use JSON to encode and then decode it:
this.displayedData = JSON.parse(JSON.stringify(this.currentData));
Both of these methods assign the current state of one value to another value, and no changes to the first value will change the second value until this code is triggered again.
The reason this is necessary isn't because of Vue.js specifically, but because of JavaScript in general. In JavaScript, arrays and objects are "passed by reference" rather than "passed by value".
An approach (WITHOUT disabling reactivity) is to use a different array for your temporary data and move stuff over in to the good array when the user presses refresh.
You could copy the values in the first array into the temp array like this.temp = this.permanent.slice() in the created or mounted life-cycle function.
Slice would make a shallow copy of the array. If you need to also clone the items in the array, then maybe use some deep copy library or the JSON.parse(JSON.stringify(...)) method.
If you DON'T add the currentData to the data() method then it will not be reactive.
export default {
currentData: [],
data() {
return {
}
},
methods: {
refresh: function() {
this.displayedData = this.currentData;
}
}
}
You can then still reference currentData in the section using {{ $options.currentData }}
You can use destructuring assigment:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment?retiredLocale=it
this.displayedData = {...this.currentData}
You will have a new object made with the "copied" data from the first
Related
I have a state like this
const [tmp,setTmp] = useState(
{status:false},
{status:false},
{status:false},
{status:false}
)
how can I change status in tmp[2] ?
You can provide a completely new list of values with the help of the spread syntax to set the new value with a different reference.
// Simply shallow copies the existing list. This is simply to make a new reference.
const newTmp = [...tmp]
// The object references inside aren't changed. But React will rerender children components anyway when the bit that uses `tmp` is rerendered.
newTmp[2].status = true
// You can also do this to get a new reference to the object
newTmp[2] = {status: true}
setTmp(newTmp)
The reason you want to provide a new value with a different reference is per React's requirement: https://reactjs.org/docs/hooks-reference.html#bailing-out-of-a-state-update
If you update a State Hook to the same value as the current state, React will bail out without rendering the children or firing effects. (React uses the Object.is comparison algorithm.)
If you simply do the following, it is not adequate since the reference to the array remains the same and React does not do deep equality check to find out if the new value is different.
tmp[2].status = true
setTmp(tmp) // Does not adhere to React docs and will probably fail at rerendering
P.S. Among the JS data types, Array, Function, and Object are references. Do read up on checking equality with references if this isn't familiar to you.
As long as the array is a different reference to the existing, React will detect this as a change. So you can clone the array, and then update the item at the index like shown by Daniel.
Or you can even use the normal JS array method slice to build a new array.
eg.
setTmp([...tmp.slice(0, 2), {status: true}, ...tmp.slice(3)]);
If you do this a lot, you could convert the above into a simple helper function.
function changeArrayItem(arr, ix, value) {
return [...arr.slice(0, ix), value, ...arr.slice(ix +1)];
}
//use
setTemp(changeArrayItem(tmp, 2, {status:true});
I have an array or objects named "properties"
This properties array has objects in the following format:
{
"some-unique-key-A": {
name:"someName1",
value: {
"some-unique-key-B": {
name:"someName11",
value : "someValue2"
}
}
}
}
Here we don't know what is the nesting-level of that key value pairs. We just know one thing, data is to be updated for key : "some-unique-key-X"
In this case how can we update the x-level nested data immutably?
Use a flat one level state of props ! Immutable is not easy to recurse, normalize your state with one level, easy to connect ;)
This is how I solved this
I created a recursive function which will accept a 'deep copy' (not shallow copy) of some object and the unique key in which value needs to be updated. It updation is done for the given key, it returns the updated object to be stored as the value of previous recursive object's value which was waiting for the result. In the end it just replaces the original state's object with the new one. I don't know if it mutates the state or not (perhaps it do mutates), but it works.
I have 2 JS objects, each is rendered as Tree in a webpage.
My issue is how to force a change on one of them while user applies a change to others.
My basic idea is to "bind onChange" on each objects obviously paying attention to not generate infinite loops.
In jQuery it seems almost difficult, I read something about "proxy" but I don't understand if it could help me on this topic.
I lastly thought to vue.js. I read that vue.js is very efficient syncing js and dom objects so a change between them is almost easy, maybe is possible to sync two js objects?
To be clearer, here more details:
I have something like this:
let obj1={key1:1, key2:[1,2,3]}; // defines arbitrary data obj
let obj2={};
$.extend(obj2,obj1); // defines obj2 as clone of obj1
// do "something magic" here
I would like to get the following:
obj1.key1=2; // => should automatically set obj2.key1=2; under the hood
obj2.key2.push(4); // => should automatically set obj1.key2=[1,2,3,4] under the hood
Is there any trick to bind two (identical, cloned) data objects so that any change made on one of them is reflected to the other one, as if the involved object keys "pointed" to the same data? Since objects are assigned "by reference" in javascript, this is doable if we define a third object "obj_value" and we assign it as value to the above objects as follows:
obj1.key=obj_value; // both obj1.key and obj2.key point to the same object
obj2.key=obj_value;
But I'd like something more general, directly binding one obj key to the other, in pseudo-code:
obj1.on('change',function(key,value)
{
obj2.key=value;
})
watch: {
objChangedByUser: function (value) {
this.cloneOfobjChangedByUser = Object.assign({}, value);
}
}
Or:
computed: {
cloneOfObjChangedByUser: function () {
return Object.assign({}, this.objChangedByUser);
}
}
document.getElementById("submit").addEventListener("click", getElements)
function getElements() {
var a = document.getElementById("sample").value;
var x = new obj(a);
function store() {
localStorage.setItem('todays-values', Object.values(x));
}
store();
}
In a separate js file I then call
localStorage.getItem('todays-values');
I get the values, but if I put new inputs into my html file and click the submit button, the previous values get overwritten and replaced by the new ones. How do I store all the values that are submitted and prevent the old ones from getting replaced?
I'm very new to Javascript so I would prefer to solve this problem without the use of any additional libraries if possible.
First: it seems that you are mixing JavaScript a class with a function (here is an example: What techniques can be used to define a class in JavaScript, and what are their trade-offs?)
For example this is the class equivalent in JavaScript:
function ClassName() {
var privateVar;
this.publicVar;
function privateFunction() {}
this.publicFunction = function() {};
}
You shouldn't wrap a function in a function unless it has a meaning (beacuse it is confusing for other people otherwise), but in the example given you don't need that. Also I can't see the reason why you are creating a new object x - if you create the object right before you save it you could just save the value because the object will only contain the value from sample, so you could write something like this:
document.getElementById("submit").addEventListener("click", getElements);
function storeElements() {
var sampleValue = document.getElementById("sample").value;
localStorage.setItem('todays-values', sampleValue);
}
Back to your question:
As Kalamarico mentioned: if you write new values into todays-values you will overwrite your old values, you could simply load all old values from the localStorage append the new ones and write them back to the localStorage.
You should also note that the localStorage only takes strings, so you should stringify objects (see localStorage.setItem).
function appendValueToStorage(key, value) {
var values = JSON.parse(localStorage.getItem(key));
if (values === null) {
values = [];
}
values.push(value);
localStorage.setItem(key, JSON.stringify(values));
console.log(localStorage.getItem(key));
}
appendValueToStorage('todays-values', document.getElementById("sample").value);
The function will let you append some value for a key, you could even wrap this function again to be able to use it in your click function:
function onSubmitClick() {
appendValueToStorage('todays-values', document.getElementById("sample").value);
}
document.getElementById("submit").addEventListener("click", onSubmitClick);
With the console.log command you can see the current content of the localStorage (you could also check with the developer tools - I find the ones for chrome work the best, under the Application -> Local Storage tab you can check the localStorage of your page).
You need read more about localStorage, this is a new feature introduced with HTML5, you can take a look here and see all features.
localStorage stores your data like a JSON object, if you don't know what is JSON, you need to find info. In javascript think in objects in this way:
var myData = {
myName: 'Kalamarico',
myAge: undefined
};
This is a Javascript object, and JSON is very similar and it is a representation of objects.
localStorage API stores your data as this way, when you do:
localStorage.setItem('todays-values', Object.values(x))
localStorage saves a new entry, one key 'todays-values' and its value is an object, so, your localStorage seems:
{
"todays-values": { ... }
}
Every time you set a "todays-values" you will overwrite the key, as you are seeing, so, if you can keep old values, you need to do this manage, first you can get items in localstorage (if there are), and after you can "merge" your old value and the new value. Or you can set a new key, for example: "todays-values1" depends on your need.
If you need to store exactly one key-value pair per day, then you could add the date in the key string.
Else how about numbering the keys ("yourKey_0", "yourKey_1", ...) and also storing the current (biggest) index ("currentIndex")in local storage:
function store(value) {
newIndex = localStorage.getItem("currentIndex") + 1;
localStorage.setItem("yourKey_" + newIndex, value);
localStorage.setItem("currentIndex", newIndex);
}
If you run into problems storing integer values, convert to strings.
I have $scope.myArray, and it's binding with an input field by ngModel and the expression {{myArray}}
My issue is when I modified myArray by call changeMyArray(), the input's value did not change. But the expression {{myArray}} is display new value.
So, Why the expression work but input field does not?
I have a way to do, but I want to find a better approach
var newArr = $scope.myArray;
newArr.push("b");
$scope.myArray = angular.copy(newArr);;
Example fiddle
Basically, I think what you want to do is bind the input to a "new entry" scope variable, and then push the value of that variable to your array when the user clicks "Push To". Here's what I mean:
In controller:
$scope.changeMyArray = function() {
$scope.myArray.push($scope.newEntry);
$scope.newEntry = "";
}
In HTML:
<input ng-model="newEntry">
But actually:
Really what you want is a way to edit the contents of an array via text, and have updates to that array from elsewhere also update the text. This is actually pretty simple since browsers come with a JSON library.
I implemented it by starting with a known pair of objects:
$scope.myArray = [];
$scope.myArrayString = "[]";
That way you can update the string via ngModel:
<input ng-model="myArrayString">
Watch for changes on this model to update the actual array:
$scope.$watch("myArrayString", function() {
$scope.myArray = JSON.parse($scope.myArrayString);
});
Then update the string in the changeMyArray function:
$scope.changeMyArray = function() {
$scope.myArray.push("b"); // Or whatever you would like to add here
$scope.myArrayString = JSON.stringify($scope.myArray);
}
Experiment in my fork of the Fiddle.
What's going on?
The variable $scope.myArray is an object, and any object in Javascript can be converted to a string (most complex objects end up as the unhelpful "[object Object]"). Arrays will actually display their contents when converted to a string, so binding an array to HTML via {{myArray}} is pretty straightforward.
However, the reverse conversion is not as simple. In general, a text input can't be bound to an array in a two-way fashion as we'd like. The solution, then, is to use an intermediary variable to hold the string value, and use $scope.$watch to keep the two values in sync.
So you seem to be wondering why when pushing to the array, your $watch function doesn't do the increment. That's because the #watch function only checks object reference equality.
When pushing to the array, the reference stays the same. When you copy the array and set it again in the same variable, the reference changes.
That's why #watchCollection works as expected and increments when each item is pushed.
I have an explanation for my question. Please correct me if I wrong, very thank.
My Question:
Why "myArray" input field does not update when $scope.myArray is changed (Model doesn't update View)?
<input ng-model="myArray" id="myArray">
The answer is AngularJs ng-model doesn't know $scope.myArray is changed. Because ng-model does not perform a deep watch of object (rather than a string or number), it only looks for a change of identity or compares the reference of the objects.
In my case, $scope.myArray is collection. So, although $scope.myArray has changed by push new item (structure is changed), it's reference does not change.
As the result, $setViewValue() and $render() never invoked to update the view.
$render
$setViewValue
Solution:
Sol1: Add new item to $scope.myArray, make a copy of myArray object and then asign a copy to $scope.myArray again. By this way, the object reference is changed. AngularJs see that change and update the view.
var newArr = $scope.myArray;
newArr.push("b");
$scope.myArray = angular.copy(newArr);
Sol2: Create custome $watch('email', function(){...}, true). The last parameter is TRUE to let Angular perform a deep watch. Then, in watch's listener function, I manually set $viewValue = newValue and invoke $render() of ngModelController to update the view. In case we have Formatters, we should invokes them in this step.
$scope.$watch('myArray', function(newValue, oldValue) {
if (newValue !== oldValue) {
var ctrl = angular.element(document.querySelector('#myArray')).controller('ngModel');
// Invoke formatter
var formatters = ctrl.$formatters,
idx = formatters.length;
while(idx--) {
newValue = formatters[idx](newValue);
}
ctrl.$render();
}
}, true);
Please see my script