Skip to content

Instantly share code, notes, and snippets.

@jr314159
Created March 20, 2013 13:51
Show Gist options
  • Save jr314159/5204784 to your computer and use it in GitHub Desktop.
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
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
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