does anyone have a suggestion for the best way to deep convert a js list of lists to nested ordered maps with Immutable.js?
You can create your own custom conversion. For example, to turn JS objects into Immutable.OrderedMap:
function fromJSOrdered(js) {
return typeof js !== 'object' || js === null ? js :
Array.isArray(js) ?
Immutable.Seq(js).map(fromJSOrdered).toList() :
Immutable.Seq(js).map(fromJSOrdered).toOrderedMap();
}
The fromJS has a second parameter called reviver, which can be exactly used for this.
import Immutable from 'immutable';
const reviver = (key, value) =>
Immutable.Iterable.isKeyed(value) ? value.toOrderedMap() : value.toList();
const data = Immutable.fromJS(js, reviver);
the response by #Albert Olivé had problems on my case use, because of recursion and lack of main context. I've tried with a second context argument to map, with problems again.
Finally I realized that I didn't care about order in submaps, just in main map passed to the function, to maintain order provided by server on html lists. So I changed the function to no recursive this way:
fromJSOrdered(js) {
if (typeof js !== 'object' || js === null || Array.isArray(js)) {
return fromJS(js);
}
return new OrderedMap(fromJS(js));
}
I actually published a package recently that can transform an object, array or Map of objects into Immutable List's and Record's:
https://github.com/jhukdev/immutable-parsejs
You can take a look at the source if you don't want Record's, easy to change.
Record's are nice though, as you have direct property access, meaning in future if you wanted to switch away from ImmutableJs, it's an easier prospect
Related
I have a model:
export default Model.extend({
title: attr('string'),
attributes: attr('jsonb')
});
Where attributes is a custom json filed stored as jsonb in Postgres.
let say:
{
"name":"Bob",
"city":""
}
So I can easily manipulate attributes using template
<form.element .. #property="attributes.city"/> or model.set('attributes.city','city name')
Problem: hasDirtyAttributes do not changing because technically we have old object. But when I try to copy object let say
JSON.parse(JSON.stringify(this.get('attributes')) hasDirtyAttributes works as expected
So how to write some Mixin for a Model or other workaround which on the change of any attribute property will mark hasDirtyAttributes as true. I will update whole object so doesn't matter which property actually was changed.
Same problem: https://discuss.emberjs.com/t/hasdirtyattributes-do-not-work-with-nested-attributes-json-api/15592
existing solution doesn't work for me at all:
ember-dirtier
ember-data-relationship-tracker
ember-data-model-fragments (a lot of changes under the hood and broke my app)
Update:
Some not perfect idea that help better describe what I'm want to achieve:
Let say we adding observer to any object fileds:
export default Model.extend({
init: function(){
this._super();
this.set('_attributes', Object.assign({}, this.get('attributes'))); //copy original
Object.keys(this.get('attributes')).forEach((item) => {
this.addObserver('attributes.'+ item, this, this.objectObserver);
});
}
...
})
And observer:
objectObserver: function(model, filed){
let privateFiled = '_' + filed;
if (model.get(privateFiled) != model.get(filed)) { //compare with last state
model.set(privateFiled, this.get(filed));
model.set('attributes', Object.assign({}, this.get('attributes')) );
}
}
It's works, but when I change one filed in object due to copying object objectObserver faired again on every filed. So in this key changing every filed in object I mark observed filed as dirty
The further ember development will reduce using of event listener and two-way binding, actually Glimmer components supports only One-way Data Flow. So to be friendly with future versions of emberusing one-way data flow is good approach in this case to. So In my case as I use ember boostrap solution looks like
<form.element #controlType="textarea" #onChange={{action 'attributeChange'}}
where attributeChange action do all works.
New Glimmer / Octane style based on modifier and looks like:
{{!-- templates/components/child.hbs --}}
<button type="button" {{on "click" (fn #onClick 'Hello, moon!')}}>
Change value
</button>
Immutable object can be an instance of:
Immutable.List
Immutable.Map
Immutable.OrderedMap
Immutable.Set
Immutable.OrderedSet
Immutable.Stack
There is an open ticket to improve the API which is on the roadmap for 4.0. Until this is implemented, I suggest you use Immutable.Iterable.isIterable() (docs).
Using instanceof is not reliable (e. g. returns false when different modules use different copies of Immutable.js)
I have learned that using instanceof to determine wether object is Immutable is unsafe:
Module A:
var Immutable = require('immutable');
module.exports = Immutable.Map({foo: "bar});
Module B:
var Immutable = require('immutable');
var moduleA = require('moduleA');
moduleA instanceof Immutable.Map // will return false
Immutable.js API defines the following methods to check if object is an instance of Immutable:
Map.isMap()
List.isList()
Stack.isStack()
OrderedMap.isOrderedMap()
Set.isSet()
OrderedSet.isOrderedSet()
and
Iterable.isIterable()
The latter checks if:
True if an Iterable, or any of its subclasses.
List, Stack, Map, OrderedMap, Set and OrderedSet are all subclasses of Iterable.
Immutable.js has isImmutable() function since v4.0.0-rc.1:
import { isImmutable, Map, List, Stack } from 'immutable';
isImmutable([]); // false
isImmutable({}); // false
isImmutable(Map()); // true
isImmutable(List()); // true
isImmutable(Stack()); // true
isImmutable(Map().asMutable()); // false
If you use one of the previous versions, you can check if object is Immutable this way:
Immutable.Iterable.isIterable(YOUR_ENTITY)
because all immutables inherit from the Iterable object
And this way you can get to know what type of Immutable Iterable variable is:
const obj0 = 'xxx';
const obj1 = Immutable.fromJS({x: 'XXX', z: 'ZZZ'});
const obj2 = Immutable.fromJS([ {x: 'XXX'}, {z: 'ZZZ'}]);
const types = ['List', 'Stack', 'Map', 'OrderedMap', 'Set', 'OrderedSet'];
const type0 = types.find(currType => Immutable[currType][`is${currType}`](obj0));
const type1 = types.find(currType => Immutable[currType][`is${currType}`](obj1));
const type2 = types.find(currType => Immutable[currType][`is${currType}`](obj2));
console.log(`Obj0 is: ${type0}`); // Obj0 is: undefined
console.log(`Obj1 is: ${type1}`); // Obj1 is: Map
console.log(`Obj2 is: ${type2}`); // Obj2 is: List
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.1/immutable.js"></script>
Checking specific types will generally cause more work later on. Usually I would wait to lock types in by checking for Map or List, but...
My motivation here is mostly that my call .get of undefined poops itself really hard, and initializing properly all over the place helps, but doesn't catch all edge cases. I just want the data or undefined without any breakage. Specific type checking causes me to do more work later if I want it to make changes.
This looser version solves many more edge cases(most if not all extend type Iterable which has .get, and all data is eventually gotten) than a specific type check does(which usually only saves you when you try to update on the wrong type etc).
/* getValid: Checks for valid ImmutableJS type Iterable
returns valid Iterable, valid Iterable child data, or undefined
Iterable.isIterable(maybeIterable) && maybeIterable.get(['data', key], Map()), becomes
getValid(maybeIterable, ['data', key], Map())
But wait! There's more! As a result:
getValid(maybeIterable) returns the maybeIterable or undefined
and we can still say getValid(maybeIterable, null, Map()) returns the maybeIterable or Map() */
export const getValid = (maybeIterable, path, getInstead) =>
Iterable.isIterable(maybeIterable) && path
? ((typeof path === 'object' && maybeIterable.getIn(path, getInstead)) || maybeIterable.get(path, getInstead))
: Iterable.isIterable(maybeIterable) && maybeIterable || getInstead;
//Here is an untested version that a friend requested. It is slightly easier to grok.
export const getValid = (maybeIterable, path, getInstead) => {
if(valid(maybeIterable)) { // Check if it is valid
if(path) { // Check if it has a key
if(typeof path === 'object') { // Check if it is an 'array'
return maybeIterable.getIn(path, getInstead) // Get your stuff
} else {
maybeIterable.get(path, getInstead) // Get your stuff
}
} else {
return maybeIterable || getInstead; // No key? just return the valid Iterable
}
} else {
return undefined; // Not valid, return undefined, perhaps should return false here
}
}
Just give me what I am asking for or tell me no. Don't explode. I believe underscore does something similar also.
This may work in some cases:
typeof object.toJS === 'function'
You can use this ducktyping method if you check immutable vs plain objects (json), for example.
Consider a REST URL like /api/users/findByCriteria which receives POSTed JSON that contains details of the criteria, and outputs a list of Users.
How would one call this with Restangular so that its results are similar to Restangulars getList()?
Restangular.all('users').post("findByCriteria", crit)... might work, but I don't know how to have Restangular recognize that the result will be a list of Users
Restangular.all('users').getListFromPOST("findByCriteria", crit)... would be nice to be able to do, but it doesn't exist.
Doing a GET instead of a POST isn't an option, because the criteria is complex.
Well,
I experience same problem and I workaround it with plain function, which return a plain array of objects. but it will remove all Restangular helper functions. So, you cant use it.
Code snippet:
Restangular.one('client').post('list',JSON.stringify({
offset: offset,
length: length
})).then(
function(data) {
$scope.clients = data.plain();
},
function(data) {
//error handling
}
);
You can get a POST to return a properly restangularized collection by setting a custom handler for OnElemRestangularized in a config block. This handler is called after the object has been Restangularized. isCollection is passed in to show if the obect was treated as a collection or single element. In the code below, if the object is an array, but was not treated as collection, it is restangularized again, as a collection. This adds all the restangular handlers to each element in the array.
let onElemR = (changedElem, isCollection, route, Restangular: restangular.IService) => {
if (Array.isArray(changedElem) && !isCollection ) {
return Restangular.restangularizeCollection(null, changedElem, changedElem.route);
}
return changedElem;
};
RestangularProvider.setOnElemRestangularized(onElemR);
I can implement Mvvm with Knockout.js. But I want to use it with cross browser(FF and Chrome) supported Html 5 offline storage.
I want to bind html objects to offline storage.
I haven't tried it, but there is a knockout.localStorage project on GitHub, that seems to be what you are looking for.
With that plugin, you should be able to pass an object as a second argument, when you create your observable, which saves the observable into localStorage.
From the documentation:
var viewModel = {
name: ko.observable('James', {persist: 'name'})
}
ko.applyBindings(viewModel);
You can use a library such as amplify.js which can serialize objects to localStorage (cross browser). It falls back to older storage tools for older browsers too. First, unwrap the observables to a JSON object, then use amplify.store to serialize the object and store it. Then you can pull it back out and map it back to an observable object when you want to fetch it.
http://amplifyjs.com/api/store/
http://craigcav.wordpress.com/2012/05/16/simple-client-storage-for-view-models-with-amplifyjs-and-knockout/
His solution works!
I worked out a solution bases on the subscribe feature of KnockoutJS. It takes a model and persist all the observable properties.
ko.persistChanges = function (vm, prefix) {
if (prefix === undefined) {
prefix = '';
}
for (var n in vm) {
var observable = vm[n];
var key = prefix + n;
if (ko.isObservable(observable) && !ko.isComputed(observable)) {
//track change of observable
ko.trackChange(observable, key);
//force load
observable();
}
}
};
Check http://keestalkstech.com/2014/02/automatic-knockout-model-persistence-offline-with-amplify/ for code and JSFiddle example.
I would like to know if any better way to create multiple collections fetching from a single big JSON file. I got a JSON file looks like this.
{
"Languages": [...],
"ProductTypes": [...],
"Menus": [...],
"Submenus": [...],
"SampleOne": [...],
"SampleTwo": [...],
"SampleMore": [...]
}
I am using the url/fetch to create each collection for each node of the JSON above.
var source = 'data/sample.json';
Languages.url = source;
Languages.fetch();
ProductTypes.url = source;
ProductTypes.fetch();
Menus.url = source;
Menus.fetch();
Submenus.url = source;
Submenus.fetch();
SampleOne.url = source;
SampleOne.fetch();
SampleTwo.url = source;
SampleTwo.fetch();
SampleMore.url = source;
SampleMore.fetch();
Any better solution for this?
Backbone is great for when your application fits the mold it provides. But don't be afraid to go around it when it makes sense for your application. It's a very small library. Making repetitive and duplicate GET requests just to fit backbone's mold is probably prohibitively inefficient. Check out jQuery.getJSON or your favorite basic AJAX library, paired with some basic metaprogramming as following:
//Put your real collection constructors here. Just examples.
var collections = {
Languages: Backbone.Collection.extend(),
ProductTypes: Backbone.Collection.extend(),
Menus: Backbone.Collection.extend()
};
function fetch() {
$.getJSON("/url/to/your/big.json", {
success: function (response) {
for (var name in collections) {
//Grab the list of raw json objects by name out of the response
//pass it to your collection's constructor
//and store a reference to your now-populated collection instance
//in your collection lookup object
collections[name] = new collections[name](response[name]);
}
}
});
}
fetch();
Once you've called fetch() and the asyn callback has completed, you can do things like collections.Menus.at(0) to get at the loaded model instances.
Your current approach, in addition to being pretty long, risks retrieving the large file multiple times (browser caching won't always work here, especially if the first request hasn't completed by the time you make the next one).
I think the easiest option here is to go with straight jQuery, rather than Backbone, then use .reset() on your collections:
$.get('data/sample.json', function(data) {
Languages.reset(data['Languages']);
ProductTypes.reset(data['ProductTypes']);
// etc
});
If you wanted to cut down on the redundant code, you can put your collections into a namespace like app and then do something like this (though it might be a bit too clever to be legible):
app.Languages = new LanguageCollection();
// etc
$.get('data/sample.json', function(data) {
_(['Languages', 'ProductTypes', ... ]).each(function(collection) {
app[collection].reset(data[collection]);
})
});
I think you can solve your need and still stay into the Backbone paradigm, I think an elegant solution that fits to me is create a Model that fetch the big JSON and uses it to fetch all the Collections in its change event:
var App = Backbone.Model.extend({
url: "http://myserver.com/data/sample.json",
initialize: function( opts ){
this.languages = new Languages();
this.productTypes = new ProductTypes();
// ...
this.on( "change", this.fetchCollections, this );
},
fetchCollections: function(){
this.languages.reset( this.get( "Languages" ) );
this.productTypes.reset( this.get( "ProductTypes" ) );
// ...
}
});
var myApp = new App();
myApp.fetch();
You have access to all your collections through:
myApp.languages
myApp.productTypes
...
You can easily do this with a parse method. Set up a model and create an attribute for each collection. There's nothing saying your model attribute has to be a single piece of data and can't be a collection.
When you run your fetch it will return back the entire response to a parse method that you can override by creating a parse function in your model. Something like:
parse: function(response) {
var myResponse = {};
_.each(response.data, function(value, key) {
myResponse[key] = new Backbone.Collection(value);
}
return myResponse;
}
You could also create new collections at a global level or into some other namespace if you'd rather not have them contained in a model, but that's up to you.
To get them from the model later you'd just have to do something like:
model.get('Languages');
backbone-relational provides a solution within backbone (without using jQuery.getJSON) which might make sense if you're already using it. Short answer at https://stackoverflow.com/a/11095675/70987 which I'd be happy to elaborate on if needed.