简体   繁体   中英

ember.js rest JSON recursively nested with same model

below is an example of the JSON I get from my REST endpoint. I need to create a tree-like structure from the subcategories arrays, which, as you can see, are recursively nested.

I searched and tried for almost a week now, but am unable to come up witha fully working solution -- the best I got so far was that the subcategories appeared both as children of their parent subcategory (as desired), but also as normal top-level nodes, which I clearly do not want.

my question is simply: how do I model this so that ember.js is able to create the desired tree-like structure?

I don't think this is very difficult for an experienced ember user, but as usual ember's documentation it is neither up-to-date nor does it cover more than trivial concepts.

wherever I look, the concept of such a nested structure seems to be alien (all nesting I find is with different types, but that is exactly what I don't need). the JSON format is immutable, I have to work with it the way it is.

I created my project with - ember-cli 0.2.7 - ember.js 1.13.0 - ember-data 1.13.4 using DS.RESTAdapter and DS.RESTSerializer

{
"id": 28,
"hassubcategories": true,
"CategoryName": "By Asset Type",
"subcategories": [
    {
        "id": 29,
        "CategoryName": "Images",
        "hassubcategories": true,
        "subcategories": [
            {
                "id": 30,
                "CategoryName": "Illustrations",
                "hassubcategories": false
            },
            {
                "id": 31,
                "CategoryName": "Pictures",
                "hassubcategories": true,
                "subcategories": [
                    {
                        "id": 61,
                        "CategoryName": "BMP",
                        "hassubcategories": false
                    },
                    {
                        "id": 32,
                        "CategoryName": "EPS",
                        "hassubcategories": false
                    }
                ]
            }
        ]
    },
    {
        "id": 73,
        "CategoryName": "InDesign  (related)",
        "hassubcategories": false
    }
]

}

and this should render in a template to a tree-like list Images - Illustrations -- BMP -- EPS Indesign (related)

thanks for any help.

EDIT 2015-07-17: i am still not closer to a dynamic solution, but for the moment (thankfully, the levels are limited to two) i am happy with creating a subsubcategories model and sideload. i iterate over the payload, get the ids of each subcategory (2nd level) and move that subcategory to a list. my serializer subcategory:

import DS from 'ember-data';
import ember from 'ember';

export default DS.RESTSerializer.extend({
    isNewSerializerAPI: true,
    primaryKey: 'id',
    normalizeHash: {
    subcategories: function(hash) {
        if(ember.get(hash, 'hassubcategories') && !ember.isEmpty(ember.get(hash, 'subcategories'))) {
            var ids = [];
            ember.get(hash, 'subcategories').forEach(function(cat){
                ids.push(ember.get(cat, 'id'));
            });
            hash.children = ids;
        }

        return hash;
    }
    },
    extractMeta: function(store, type, payload) {
        if (payload && payload.subcategories) {

            var subs = [];
            var subs2 = [];

            payload.subcategories.forEach(function(cat){
                if(cat['hassubcategories']) {
                    var subsubs = cat['subcategories'];
                    subs.addObject(cat);
                    subs2.addObjects(subsubs);
                } else {
                    subs.addObject(cat);
                }
            });

            payload.subcategories = subs;
            payload.subsubcategories = subs2;
        }

        delete payload.id;  // keeps ember data from trying to parse "id" as a record (subcategory)
    delete payload.hassubcategories;  // keeps ember data from trying to parse "hassubcategories" as a record (subcategory)
    delete payload.CategoryName;  // keeps ember data from trying to parse "CategoryName" as a record (subcategory)
    delete payload.ParentID;  // keeps ember data from trying to parse "ParentID" as a record (subcategory)
    }
});

and the models subcategory:

import DS from 'ember-data';

export default DS.Model.extend({
    CategoryName: DS.attr('string'),
    hassubcategories: DS.attr('boolean'),
    children: DS.hasMany('subsubcategories', {async: false })
});

subsubcategory:

import DS from 'ember-data';

export default DS.Model.extend({
    CategoryName: DS.attr('string'),
    hassubcategories: DS.attr('boolean'),
});

maybe it helps somebody and maybe i even get a hint how to it truly dynamically.

I will ramble about the solution without showing much code, if you don't mind... :D

In Ember Data (which I assume you are using), the store contains all the objects of a type in a flat collection (conceptually). Each object is retrieved by key ( id ). Ember Data provides relationship attributes to model a hasMany or belongsTo relationship. So, to connect these objects in a tree structure, you will have a hasMany from a parent node to its children:

App.Category = Ember.Model.extend({
    name: DS.attr(),
    subcategories: DS.hasMany('category'),
    parent: DS.belongsTo('category')
});

So, in your JSON example above, imagine you have Category model records like this:

[{
  id: 28,
  name: "By Asset Type",
  subcategories: [ 29, 73 ],
  parent: null
}, {
  id: 29,
  name: "Images",
  subcategories: [ 30, 31 ],
  parent: 28
}, {
  id: 73,
  name: "InDesign (related)",
  subcategories: [],
  parent: 28
}, {
// ... etc ...

So you can see from this array of objects, it is not physically nested, but nested logically via IDs. If your JSON looked like that, it could be processed by the RESTAdapter and all is good. (I hope at this point you agree and are nodding your head). Problem is, your JSON is, as you say, "immutable" which I assume means is not going to change in format.

Serializer to the rescue! By creating a custom CategorySerializer (which extends RESTSerializer ), you can override the extractArray and extractSingle functions to flatten the JSON payload into an array as I have shown above. I will leave the code for flattening the payload as an exercise for the reader. :)

You are lucky that you are getting all the data in the tree at one time and don't have to deal with making intermediate REST calls and dealing with promise resolution. This is certainly doable, but its asynchronous nature makes it a little tricky.

Some light reading:

Your next issue may be (as it has been for me), how to make each node in the tree respond to its own URL (/categories/61) and display it in context with its ancestors and siblings.

As an updated answer to this problem, it looks like this is now available via extending the DS.JSONSerializer with the DS.EmbeddedRecordsMixin .

For instance, a category model that contains subcategories array with infinitely nested categories would look like this:

// app/model/category.js
import Ember from "ember";
import Model from "ember-data/model";
import attr from "ember-data/attr";
import { belongsTo, hasMany } from 'ember-data/relationships';

export default Model.extend({
  name: attr(),
  subcategories: hasMany('category', { inverse: 'parent' }),
  parent: belongsTo('category', { inverse: 'subcategories' })
});

And your application adapter should look similar to this:

// app/adapters/application.js
import DS from "ember-data";
import config from "../config/environment";
import Ember from "ember";

export default DS.RESTAdapter.extend({

});

And your application serializer should look similar to this:

// app/serializer/application.js
import DS from 'ember-data';

export default DS.JSONSerializer.extend({

});

And finally, your category serializer as such:

// app/serializers/category.js
import ApplicationSerializer from './application';
import DS from 'ember-data';
export default ApplicationSerializer.extend(DS.EmbeddedRecordsMixin,{
  attrs: {
    subcategories: {
      serialize: 'records',
      deserialize: 'records'
    }
  }
});

This setup for your Models is documented as Reflexive Inverses here: https://guides.emberjs.com/v2.11.0/models/relationships/#toc_reflexive-relations

And documentation for the EmbeddedRecordsMixin may be found here: http://emberjs.com/api/data/classes/DS.EmbeddedRecordsMixin.html

This should allow your nested response to work, out of the box with Ember Data. It made it all magical for me.

Hope this helps someone.

Good luck.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM