Created
March 20, 2013 13:51
-
-
Save jr314159/5204784 to your computer and use it in GitHub Desktop.
Another relational model implementation for Backbone, designed to work with nested attributes in Rails
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Backbone.Localytics.Models.SuperModel extends Backbone.Model | |
#modelName: null # The overridden sync method looks to see if this is set for including root in JSON | |
# Define how JSON properties should be parsed to Backbone models or collections: | |
# associations: | |
# kitties: Cats | |
# dog: Dog | |
# Parse nested data for associated models from the attributes hash into respective collections or models | |
# If no arguments are provided, parse all associations, otherwise parse only the provided associations | |
parseAssociated: -> | |
keys = if not _.isEmpty(arguments) then arguments else _.keys(@associations) | |
_.each keys, (key) => | |
# If the associated collection or model has already been initialized, update them | |
if @[key]? | |
if @[key] instanceof Backbone.Model | |
@[key].set(@[key].parse(@get(key))) if @has(key) | |
else if @[key] instanceof Backbone.Collection | |
@[key].update(@get(key), {parse: true}) | |
# Otherwise create a new model or collection | |
else | |
@[key] = new (@associations[key])(@get(key), {parse: true}) | |
@[key].parent = this # Save a pointer | |
# Callback to bubble up change events on associated models/collections | |
_bubbleAssociatedChanges: (event) -> | |
if event in ['remove', 'change'] # These are the events that constitute a "change" on the parent model. Todo: make these configurable | |
@trigger 'change' # Should we name this to a different event to avoid potential conflicts? | |
initialize: (attributes, options) -> | |
# Parse associated models and collections and bind to change events to auto update parsed models and collections | |
@parseAssociated() | |
_.each @associations, (model, key) => | |
@on "change:#{key}", => @parseAssociated.apply(this, [key]) | |
@listenTo @[key], 'all', @_bubbleAssociatedChanges | |
toJSON: (options = {}) -> | |
_.defaults options, | |
includeAssociations: true # it might be cool to use Rails' :only or :exclude syntax here instead | |
jsonRoot: null | |
json = _.omit super, _.keys(@associations) # Filter out the nested attributes | |
if options.includeAssociations | |
_.each @associations, (model, key) => | |
json["#{key}_attributes"] = @[key].toJSON() # Name these for Rails' accepts_nested_attributes_for | |
if options.jsonRoot? then _.obj options.jsonRoot, json else json # This _.obj thing is a Localytics extension | |
sync: (method, model, options) -> # Include root in JSON if defined on the model | |
if @modelName? and not options.jsonRoot? then options.jsonRoot = @modelName | |
super(method, model, options) | |
_.mixin | |
# convenience for when you want to create an object with one key value pair where | |
# key is a variable. You cant do that by defining a string literal | |
obj: (key, value) -> | |
ret = {} | |
ret[key] = value; | |
ret |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
describe "SuperModel", -> | |
class Cats extends Backbone.Collection | |
class Dog extends Backbone.Model | |
class PetShop extends Backbone.Localytics.Models.SuperModel | |
associations: | |
kitties: Cats | |
dog: Dog | |
json = | |
name: "Joel's Pet Store" | |
kitties: [ | |
{ | |
name: 'Ofelia' | |
} | |
{ | |
name: 'Mr Whiskers' | |
} | |
] | |
dog: | |
name: 'Mr Uglyface' | |
breed: 'husky' | |
shop = new PetShop(json) | |
describe "initialization", -> | |
it "parses out nested attributes into associated models and collections", -> | |
expect(shop.kitties instanceof Cats).toBeTruthy() | |
expect(shop.dog instanceof Dog).toBeTruthy() | |
expect(shop.dog.get('name')).toEqual("Mr Uglyface") | |
describe "toJSON", -> | |
it "nests associated models with the key association_attributes", -> | |
output = shop.toJSON() | |
expect(output.kitties_attributes.length).toBe(2) | |
it "doesn't include the unparsed attributes", -> | |
output = shop.toJSON() | |
expect(output.kitties).toBe(undefined) | |
it "doesn't nest associated models if called with includeAssociations: false", -> | |
output = shop.toJSON({includeAssociations: false}) | |
expect(shop.kitties_attributes).toBe(undefined) | |
it "nests all the json under a root if jsonRoot is provided", -> | |
output = shop.toJSON({jsonRoot: 'shop'}) | |
expect(output.shop.name).toEqual("Joel's Pet Store") | |
describe "updating associated models", -> | |
it "triggers a change event on the parent model", -> | |
spyOn(shop, 'trigger') | |
shop.kitties.at(0).set('title', "Creativityiness") | |
expect(shop.trigger).toHaveBeenCalledWith('change') | |
shop.dog.set('breed', 'poodle') | |
expect(shop.trigger).toHaveBeenCalledWith('change') | |
describe "updating associated attributes", -> | |
it "updates the associated models from the changed attributes", -> | |
shop.set('kitties', [{name: 'Kimchee'}, {name: 'Mr Whiskers'}]) | |
expect(shop.kitties.first().get('name')).toBe("Kimchee") | |
describe "nested SuperModels", -> | |
class PetShops extends Backbone.Collection | |
model: PetShop | |
class ChainStore extends Backbone.Localytics.Models.SuperModel | |
associations: | |
franchises: PetShops | |
flagship: PetShop | |
json = | |
name: "PETCO" | |
franchises: [ | |
{ | |
name: "Joel's Pet Store" | |
kitties: [ | |
{ | |
name: 'Ofelia' | |
} | |
{ | |
name: 'Mr Whiskers' | |
} | |
] | |
dog: | |
name: 'Mr Uglyface' | |
breed: 'husky' | |
} | |
{ | |
name: "Debbie's Petland" | |
kitties: [ | |
{ | |
name: 'Baby Arugula' | |
} | |
{ | |
name: 'Bart' | |
} | |
] | |
dog: | |
name: 'Tesla' | |
breed: 'labrador' | |
} | |
] | |
flagship: | |
name: "PETCOLAND" | |
kitties: [ | |
{ | |
name: 'I hate people' | |
} | |
] | |
dog: | |
name: 'Walrus' | |
breed: 'seal' | |
petco = new ChainStore(json) | |
describe "initialization", -> | |
it "parses out nested attributes into associated models and collections", -> | |
expect(petco.flagship instanceof PetShop).toBeTruthy() | |
expect(petco.flagship.dog.get('name')).toEqual('Walrus') | |
expect(petco.franchises instanceof PetShops).toBeTruthy() | |
expect(petco.franchises.first() instanceof PetShop).toBeTruthy() | |
expect(petco.franchises.first().kitties.first().get('name')).toEqual('Ofelia') | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment