VueJS auto creation of nested object properties to simplify code - javascript

A little background on the problem
I've built a custom system to automatically watch some "store" properties that comes from a JSON of a nosql database. Nothing too complicated except the nesting (required for several reasons not discussed here) of objects.
The data structure looks like this:
{
store: {
objA: {
objB: {
prop1: 'some value'
}
}
}
}
However, since it's a nosql database that provide that store property, objB can just be NOT present after the load from the database.
Example of the template used
I have custom components that have props bound directly to that data store
<my-selector :value.sync="store.objA.objB.prop1">
</my-selector>
However, it crashes when "objB" is not present with the usual javascript error saying that it cannot get "objB" of undefined, but that is normal.
I'm trying to find a vuejs way to prepare the data for me.
Ideas
In order to counter that crash :
I can NOT use v-if in that case to mask the selector. Because that selector can be used even if the data is not yet set (example: for optional data).
I could fix the "load" function that gets the data from the database so it initialize the required properties (like objB) before assigning data to the data store. However, that would imply that my initial VueJS data object would contain these required properties as well. It's probably that I will use if I can't find any alternative solution, but I don't think that's the easiest way around because I would have to fix any incoming data before assignment.
My preferred choice would be to create a directive (or any other thing built in the template) that would add them for me if they are missing.
VueJS always evaluates the bindings value
I thought of that solution:
<my-selector v-autocreate="'store.objA.objB.prop1'" :value.sync="store.objA.objB.prop1">
</my-selector>
However the directive binding "v-autocreate" is not picked up first (checked with the debugger).
I did not find documentation relative of the order of load of directives or attributes.
I was also hoping to get all bindings of a node with the directive "bind"
function in order to NOT repeat the string, but it seems we can't get that information (I'm used to knockoutjs where we can pick all bindings assigned to a node in order to behave differently).
I would like to reach that goal but I'm not sure that it's possible (I would need something like a pre-bind / beforeBind event on directive haha) :
<my-selector v-autocreate :value.sync="store.objA.objB.prop1">
</my-selector>
Where v-autocreate would assure to do the vm.$set of the missing properties.

You could create a method that checks each property in an object path, creates it if it doesn't exist, then returns the value of the last property.
Example (not tested):
get(object, path) {
path = path.split('.')
let index = 0
const length = path.length
let val
while (object != null && index < length) {
let key = path[index++]
if(object[key] == null) {
this.$set(object, key, {})
}
object = object[key]
}
return val
}
Usage:
<my-selector :value="get(store,'objA.objB.prop1')">
</my-selector>
You may be interested in lodash's get function, which is what the code example is based on.
https://github.com/lodash/lodash/blob/master/get.js

Related

Can Javascript do dynamic properties like Swift's #dynamicMemberLookup?

In Swift we have #dynamicMemberlookup which lets us dynamically reference a property without having to first define it. Great for situations where keys names on things like maps are not known in advance. For example I can do something like this:
object.abc = 5
object.abc // -> 5
And the compiler will call a pre-defined function:
subscript(dynamicMember member: String) -> Int {
get { return someMap[member] }
set { someMap[member] = newValue
}
Effectively it's syntactical sugar. But what I'm doing at the moment is calling some user supplied Javascript from my Swift app and as part of that call I'm passing an object to the javascript which can be used to get and retrieve values.
At the moment I've defined function get(key) {...} and function set(key, value) {...} functions on the object I'm passing. This allows:
object.set("abc", 5);
object.get("abc"); // -> 5
But I'd really like the javascript to be able to do a dynamic property without knowing the name of it in advance. ie:
object.abc = 5;
object.abc; // -> 5
I've looked around but I can't see if this is possible in Javascript. Is it?
Edit
ultimately the object I pass to the javascript is a Swift object and the get and set functions I defined in the javascript are actually routing to the backing Swift object in a dynamic manner. That means that when I call the javascript I don't have to pass every single value currently stored. Which could be a large amount.
Not sure I follow you but is this what you want ?
let propName = 'abc' ;
object[propName] = 5
console.log(object[propName]) ;
you can change the property name to anything you want and it will just create, set and retrieve that property

How do I access the 'str' value in my object?

I am trying to return the value under the key 'str' in an Object but I am having trouble accessing the value.
This is what is returned in the console:
Currently I am using a map function to go over the array and just return the _str value like so:
let idx = currentArray.map(function(x) {
return x._id._str;
});
However it is still returning the value as an object. How can I get just the value of the _str key?
Here is the full array without specifying the id field. This is what is returned if you jsut return 'x' in the map function.
You've clarified that the screenshot is of x._id. So to access _str, you'd use x._id[0]._str: The _str property is in the object referenced by the 0 property (the first entry in the array x._id refers to).
Note that in general, _-prefixed properties are meant not to be accessed by code outside the code responsible for the objects in question. You don't seem to be responsible for them, so accessing those properties is likely to make your code rely on undocumented properties that may change in the next "dot" release of whatever lib you're using. It's just convention, but it's a very common convention.
If you right click on the property, most browser consoles offer the ability to copy property path.
Based on this SO post and the docs, it appears that you can probably use x._id.str.
If I understand correctly, you are receiving the str value but it is an object instead of the string literal. In other words, you are getting _str: "598..." instead of "598....". A possible solution would be to use the mongo javascript function to convert the str value to a string.
In your case, I think something like return x._id.str; may work as _id is a MongoID.ObjectID.
I've also linked the documentation below for reference.
https://docs.mongodb.com/manual/reference/method/ObjectId/
Here's a relevant SO answer as well: Convert ObjectID (Mongodb) to String in JavaScript
I think you should write x[_id]._str because _id is one of the array objects.

Updating an object in the ngrx/store

I'm using #ngrx/store for an Angular 2 app.
My store holds a list of say, Book objects. I want to update a field in one of those objects. I also happen to have an Observable of the Book instance I'm looking to update (say, selectedBook).
To do the update I intend on calling the reducer with an UpdateBookAction, and a payload of the new Book. So I make a deep copy of the existing Book object by subscribing to selectedBook and then calling Object.assign().
But when I try to write to one of the fields of the copy I get the following error. (It happens to be the same error I get if I were to try to write directly to the Book object in the store.)
Error
Cannot assign to read only property 'name' of object '#<Object>' at ViewWrappedError.BaseError [as constructor]
Code
ngOnInit() {
this.book$ = this.store.let(fromRoot.getSelectedBook);
//...
}
someFunction() {
//...
this.book$.subscribe(book => {
let updatedBook = Object.assign({}, book);
updatedBook.name = 'something else'; // <--- THIS IS WHAT THROWS
let action = new BookUpdateAction(updatedBook);
this.store.dispatch(action);
}
}
Clarification after Comments
I was under the assumption that I could have an action with a payload that was not the entire state of the store. (In fact that seems necessary, no?) I'm confident that this is the case given the documentation.
The action I'm looking to take is something like this:
Action = UPDATE, payload = {'id': 1234, 'name': 'something new'}
As mentioned, I intend on making that call like this:
this.store.dispatch(action);
Presumably under the hood, ngrx is passing my action to the reducer along with the (immutable) current state.
So from there, everything should work okay. My logic inside the reducer doesn't mutate the existing state, it simply creates a new one out of the existing state and the payload I've passed in.
The real question here is how I can reasonably build the new "objectToUpdate" such that I can pass that in as the payload.
I could do something like this:
this.book$.subscribe(book => {
let updatedBook = new Book();
updatedBook.id = book.id;
//set all other fields manually...
updatedBook.name = 'something else';
let action = new BookUpdateAction(updatedBook);
this.store.dispatch(action);
}
But we're not just talking about two fields here... what if my book has several fields? Do I have to manually build from scratch a new Book each time just to update one field?
My solution was to do a deep copy using Object.assign({}, book) (and not mutate the old one!) and subsequently make the update to solely the field I was looking to touch.
The idea of the ngrx store is to have one and only one single place of truth, which means all the objects are immutable, and the only way to change anything is to recreate everything as a whole. Also, you are probably using the ngrx freeze (https://github.com/codewareio/ngrx-store-freeze) which means that all of the objects will be created read-only so you wont be able to change any (This is good for development if you want to completely follow the redux pattern). If you remove the part where the store freezes the object, you will be able to change it, but thats not best practice.
What I would suggest you is the following: Use the ngrx observable with async pipe to put the data (in your case books) in a dumb component which can only get input and output some event. Than, inside of the dumb component you can "edit" that object by making a copy of it, and after you are done, you can emit back the changes to the smart component which is subscribed to the store and allow it to change the state via the store (commit). This way is best because it is not very common to change the whole state for a really small change (like two way binding, when user types..).
If you follow the redux pattern, than you will be able to add history, which means the store will keep a copies of the last X state recreations, so you can get UNDO functionality, easier to debug, timeline etc
Your problem is that you are directly editing the property instead of recreating the whole state.
I'll have to make an assumption about the actual scenario the OP is experiencing.
The problem
It's not possible to modify a member of a frozen object. Its the error being thrown.
The cause
ngrx-store-freeze is used as a meta-reducer to freeze any object that enters the store. On another place, when an object needs to be changed, a shallow copy is being made. Object.assign() doesn't do deep copy. A member of another object reached from the original object is being modified. This secondary object is also frozen, by it is not duplicated.
Solution
Use a deep copy like cloneDeep() from lodash. Or sent a bag of properties to be changed with a proper action. Process the changes on the reducer.
As already mentioned - the reason you are getting
Cannot assign to read only property 'name' of object
is because 'ngrx-store-freeze' freezes the state and prevents mutating it.
Object.assign will provide a new object as you expect, but it will copy the state's properties along with each property's own definition - such as the 'writable' definition (which 'ngrx-store-freeze' likely sets to false).
A different approach is described in this answer and explains how cloning objects with JSON.parse(JSON.stringify(yourObject)) as fastest, but this approach has flaws if you keep dates or methods etc' in your state.
using lodash's 'cloneDeep' is probably your best bet for deep cloning the state.
One way to accomplish this is a utility/helper method to make a new book from.
You could give it an existing book and the subset of properties you want to add to a new book (using Partial in typeScript if you want type safety).
createNewBook(oldBook: Book, newProps: Partial<Book>): Book {
const newBook = new Book();
for(const prop in oldBook) {
if(newProps[prop]) {
newBook[prop]=newProps[prop];
} else {
newBook[prop]=oldBook[prop];
}
}
return newBook
}
You could call it via newBook = createNewBook(new Book(), {title: 'first foo, then bar'});
and use this newBook to update your store.

Angular JS: how to "load" an individual record from a json dataset?

I have json data, an array of 50 objects representing people. Each one has parameters like id and lastName.
I load this into my controller via a resolve, EmployeeResolve, and into a variable _this.employees
I also load via $state params from a previous page a rowNumber variable that holds the ID of the record the user clicked on: _this.rowNum = $stateParams.id;let's say the id is 5.
I would like to assign to a variable now the object number 5 (for want of a better way of explaining) so that in my HTML I can bind to it as in {{controller.lastName}}
What's the syntax for getting the 5th item out of employees?
UPDATE
After several helpful comments and answers, I've gotten this far (people are now packages):
_this.recordNum = Number($stateParams.id);
_this.packages = PackagesResolve;
_this.currentPackage = _this.packages.filter(function(pkg) {
return pkg.id === _this.recordNum;
});
$log.debug('package from filter', _this.currentPackage[0].status);
Note though, I expected after all this for _this.currentPackage to contain an object, so I could simply bind to its props in the html as in currentPackage.last_name But it does not. It's a resource and I need to use the above _this.currentPackage[0].status in the log statement to get anything. And that's not going to allow binding.
A colleague suggested modifying my resolve as such
PackagesResolve: function($log, MockDataFactory) {
return MockDataFactory.query({filename: 'packages'}).$promise.then(function(response) {
return response;
});
}
Adding the whole $promise.then part. No real difference.
To reiterate what I am trying to do:
PackagesResolve is getting a json array of 50 objects. I want to be able to get the CURRENT object when its row in a table of that json is clicked.
And no, #jdvp it's not a duplicate of that other post at all. I need to do this with Angular, not jquery or straight js.
If I'm understanding your issue correctly: the object returned by resolve is the resolved promise. The "data" of the resolved promise, which in this case would be the expected array of people info, is stored inside resolve.data. So for e.g. you have EmployeeResolve, you can reference the array and store it using:
Editing based on comments:
// Assuming you've done all error checking...
_this.employees = EmployeeResolve.data;
// Now _this.employees has the array of people info.
$scope.controller = {};
$scope.controller.variableName = _this.employees[$stateParams.id];
// Now, you can access your object in your template using controller.variableName.
Now although I wouldn't recommend writing code like that in your final version, I'm sure you get the gist. ;)
Additional notes: The reason I'm creating an empty object and storing it as controller on the scope is because your question stated it. I am assuming you have your own reasons for wanting to namespace your variable inside of controller.
Hope this helps!

Am I overusing the Knockout mapping plugin by always using it to do my viewmodel?

I'm still learning the proper usage of Knockout and I've found myself quickly getting away from ever typing ko.observable when setting up my viewmodel and instead just defining an object literal and passing it through the mapping plugin with something like
var viewModel = ko.mapping.fromJS(data);
or at the very least, something along the lines of stuffing all of my data into an attribute on the viewModel like so
var viewModel = {
... events etc ... ,
"data": ko.mapping.fromJS(data)
}
To be honest, the main reason I've been doing this is to get around having to type ko.observable and ko.observableArray repetitively. I'm just trying to figure out if this is a good approach and if there are any downsides to dropping the specific var x = ko.observable() declaration all together. Also, I'm doing this all on load, not in response to any ajax call etc, which from what I can tell, is what the mapping plugin was designed for.
In your work with knockout, do you still declare the observables manually, one by one, or have you gone with the mapping.fromJS method that I use? Are there any specific downsides to using the mapping plugin so frequently like this?
Edit:
Specific Example
In this article, Steve sets up his viewModel by doing
var initialData = [ { ... } , { ... } ]; // json from the serializer
var viewModel = {
gifts : ko.observableArray(initialData)
};
Normally, I'd just use ko.mapping.fromJS for this situation as well, specifically to make sure the objects within the array are turned into observables as well. Looking at what he did, my approach seems like its overkill and adds a bit of unnecessary overhead.
After using Knockout for a little longer, I've noticed that the mapping plugin has some additional options that give you much more fine grained control over the mapping process.
Control type and amount of properties generated
There are several ways to accomplish this, and I'll go over some, but the end result is that you end up with a lighter result from the mapping plugin because everything isn't observable.
Basically you leave everything that you don't think will change, as a normal property and only make observables out of the specific items that you want to observe.
Make mapping omit certain properties
You can make the mapping plugin omit properties entirely from the end result by specifying things like ignore or include. Both of these accomplish the same thing, just in opposite ways.
Note: Samples are from the knockout.js mapping plugin documentation, comments added by me
Mapping Plugin Argument: include
The following snippet will omit all properties from the source object other than those passed in via the include argument.
// specify the specific properties to include as observables in the end result
var mapping = {
// only include these two properties
'include': ["propertyToInclude", "alsoIncludeThis"]
}
// viewModel will now only contain the two properties listed above,
// and they will be observable
var viewModel = ko.mapping.fromJS(data, mapping);
Mapping Plugin Argument: ignore
If you want to only omit certain properties from the source object, use the ignore argument as shown below. It will make observables from all properties in the source object except for the specified properties.
// specify the specific properties to omit from the result,
// all others will be made observable
var mapping = {
// only ignore these two properties
'ignore': ["propertyToIgnore", "alsoIgnoreThis"]
}
// viewModel will now omit the two properties listed above,
// everything else will be included and they will be an observable
var viewModel = ko.mapping.fromJS(data, mapping);
Control what properties are or are not made observable
If you need to include properties but you don't think that they will need to be made observable (for whatever reason), the mapping plugin has something that can help.
Mapping Plugin Argument: copy
If you want the mapping plugin to simply copy the plain properties and not make them observable, use this argument, as shown below.
// tell the mapping plugin to handle all other properties normally,
// but to simply copy this property instead of making it observable
var mapping = {
'copy': ["propertyToCopy"]
}
var viewModel = ko.mapping.fromJS(data, mapping);
Gain complete control over the mapping process
If you want to have 100% control over what is created in the mapping process, including the ability to put closures and subscriptions in your objects, then you want to use the "create" option.
plain result with calculated properties
Here is an example where I was mapping data from an ajax call to an object with a results property. I didn't want anything observable and I just wanted a simple generated property that would be made of the other simple properties on the object. Maybe not the most compelling example but it demonstrates the functionality.
var searchMappingConfig = {
// specific configuration for mapping the results property
"results": {
// specific function to use to create the items in the results array
"create": function (options) {
// return a new function so we can have the proper scope/value for "this", below
return new function () {
// instead of mapping like we normally would: ko.mapping.fromJS(options.data, {}, this);
// map via extend, this will just copy the properties from the returned json element to "this"
// we'll do this for a more light weight vm since every last property will just be a plain old property instead of observable
$.extend(this, options.data);
// all this to add a vehicle title to each item
this.vehicleTitle = this.Year + "<br />" + this.Make + " " + this.Model;
}, this);
};
}
}
}
subscriptions and closures and mapping, oh my
Another situation is if you want closures and subscriptions in your result. This example is too long to be included in its entirety but its for a vehicle make/model hierarchy. I wanted all the models (children) for a given make (parent) to be un-enabled if the model was un-enabled and I wanted this to be done with a subscription.
// here we are specifying the way that items in the make array are created,
// since makes has a child array (Models), we will specify the way that
// items are created for that as well
var makesModelsMappingConfig = {
// function that has the configuration for creating makes
"create": function (options) {
// return a new function so we can have the proper
// scope/value for "this", below
return new function () {
// Note: we have a parent / child relationship here, makes have models. In the
// UI we are selecting makes and then using that to allow the user to select
// models. Because of this, there is going to be some special logic in here
// so that all the child models under a given make, will automatically
// unselect if the user unselects the parent make.
// make the selected property a private variable so it can be closure'd over
var makeIsSelected = ko.protectedComputed(false);
// expose our property so we can bind in the UI
this.isSelected = makeIsSelected;
// ... misc other properties and events ...
// now that we've described/configured how to create the makes,
// describe/configure how to create the models under the makes
ko.mapping.fromJS(options.data, {
// specific configuration for the "Models" property
"Models": {
// function that has the configuration for creating items
// under the Models property
"create": function (model) {
// we'll create the isSelected as a local variable so
// that we can flip it in the subscription below,
// otherwise we wouldnt have access to flip it
var isSelected = ko.protectedComputed(false);
// subscribe to the parents "IsSelected" property so
// the models can select/unselect themselves
parentIsSelected.current.subscribe(function (value) {
// set the protected computed to the same
// value as its parent, note that this
// is just protected, not the actual value
isSelected(value);
});
// this object literal is what makes up each item
// in the Models observable array
return {
// here we're returning our local variable so
// we can easily modify it in our subscription
"isSelected": isSelected,
// ... misc properties to expose
// under the item in the Model array ...
};
}
}
}, this);
};
}
};
All in all, what I've found is that you rarely need 100% of an object that you'd pass to the plugin and you rarely need 100% of it to be observable. Dig in with the mapping configuration options and create all sorts of complex and simple objects. The idea is to only get everything you need, nothing more or less.
My suggestion to you would the same another questioned I just answered at https://stackoverflow.com/questions/7499133/mapping-deeply-hierarchical-objects-to-custom-classes-using-knockout-mapping-plug.
Your reasoning for using mapping plug-in is reasonable and the one that I use. Why type more code than you have to?
In my experience with knockout (all of 4 months), I've found that the less I do manually and let the knockout routines do their thing, the better my apps seem to run. My suggestion is try the simplest approach first. If it doesn't meet your needs, look at how the simple approach is doing it's "thing" and determine what has to change to meet your needs.
Allen, my recent learning experience with Knockout.js has been similar to yours. We work with a deep hierarchical object graph from the server and I have defined explicit instantiable view model functions which preserve the basic structure of it.
I began by defining each property explicitly as an observable on the relevant view model, but that quickly got out of hand. Also, a major reason for switching to using the mapping plugin was that we have to do frequent Ajax posts of the graph back to the server where it is merged with the persisted version, then validated on the server in such a way that numerous properties can change and collections be modified, and a new instance returned as the Ajax result where it has to be re-merged with the client representation. That became seriously difficult, and the mapping plugin helped big time by allowing the specification of identifiers for resolving adds / deletes / updates and to remap an updated graph onto the original.
It also helped in the original graph creation through the use of the "create" option for sub view models. In each view model constructor I receive a reference to the parent view model plus the data with which to construct the child view model, then create further mapping options to create grandchildren from the passed-in child data.
The only (slight) downside I recently found, as detailed in this question, is that when doing ko.mapping.toJSON it doesn't hook into any toJSON overrides you may have defined on the prototypes of your view models in order to exclude properties from serialization. I have been able to get around that by specifying ignore options in the unmapping, as recommended by Ryan Niemeyer in that post.
So in summary, I'll definitely be sticking with the mapping plugin. Knockout.js rules.
A simpler but help-full add-on could be knockout-data-projections
Currently, it does not handle js to viewmodel mappings, but it handles quite well view model to JS mappings.

Categories