Skip to content

Instantly share code, notes, and snippets.

@skurfuerst
Created January 5, 2014 11:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save skurfuerst/8267335 to your computer and use it in GitHub Desktop.
Save skurfuerst/8267335 to your computer and use it in GitHub Desktop.
SerializableMixin for Ember.JS
define [
'lib/ember'
], (
E
) ->
# # SerializableMixin
#
# Makes persistent parts of an `Ember.Object` serializable and deserializable into a string:
#
# * The property names which contribute to persistent state of the object should be stored inside the `stateProperties` array.
# * Introduces a read/writeable `state` property which contains the serialized state of the object.
# * The `state` only contains properties which are different than their initial value.
#
#
# ## Usage
#
# Just mix it into any object which shall be serializable, set `stateProperties` correctly and make sure to call `@_super()`
# in your custom `init()` method (if any).
#
# ```
# E.Object.extend SerializableMixin, {
# stateProperties: ['property1', 'property2']
# property1: 'my simple property'
# arrayProperty: (-> []).property()
# }
# ```
#
#
# ## Which properties are serializable?
#
# You can serialize both simple types like boolean, number, string, and *arrays of simple types* as well.
#
# It is currently *not supported* to serialize nested objects or array of nested objects.
#
#
# ## Specifying the Initial Value
#
# It is extremely important for the system to function to have the initial value specified on instanciation of the class.
#
# For simple types, this is usually not a problem, but arrays should be wrapped in a computed property, as in the above
# example.
#
# **The default value's type is checked, and for arrays, we are listening to `@each` array element. That's why the type of
# a persistent value is not allowed to change during its lifetime.**
SerializableMixin = E.Mixin.create {
concatenatedProperties: ['stateProperties']
# ## API: `stateProperties`
#
# all properties listed inside here are serialized inside `state`. Must not be changed after object creation.
stateProperties: []
# ## API: `state`
#
# the serialized `state` of the object. String. can be read from or written to.
state: null
# ---
# ## Internal Implementation
#
# This object contains all the default values of all `stateProperties`.
__propertyDefaults: null
init: ->
@_super()
# build up `@__propertyDefaults` and `propertiesToObserve`
@__propertyDefaults = {}
stateProperties = @get('stateProperties')
propertiesToObserve = []
for stateProperty in stateProperties
defaultValue = @get(stateProperty)
# REMINDER: we need to COPY the default value here, it might be an ARRAY which would just be referenced otherwise.
@__propertyDefaults[stateProperty] = E.copy(defaultValue)
if E.isArray(defaultValue)
propertiesToObserve.push(stateProperty + '.@each')
else
propertiesToObserve.push(stateProperty)
# add the `state` computed property
E.defineProperty(@, 'state', E.computed((k, v) ->
if arguments.length > 1
# SET
deserializedState = JSON.parse(v)
for stateProperty in stateProperties
if deserializedState.hasOwnProperty(stateProperty)
@set(stateProperty, deserializedState[stateProperty])
else
@set(stateProperty, @__propertyDefaults[stateProperty])
state = {}
for stateProperty in stateProperties
currentValue = @get(stateProperty)
if E.compare(currentValue, @__propertyDefaults[stateProperty]) != 0
# not equal
state[stateProperty] = currentValue
return JSON.stringify(state)
).property(propertiesToObserve...))
# fetch the `state` initially such that it can be observed properly. See http://emberjs.com/blog/2013/08/29/ember-1-0-rc8.html
# for an explanation.
@get('state')
}
return SerializableMixin
define([
'lib/ember'
'cs!Shared/SerializableMixin'
], (
E
SerializableMixin
) ->
A = Ember.Object.extend SerializableMixin, {
stateProperties: ['myProp']
myProp: (->
['x']
).property()
stateChangeEvents: 0
stateObserver: (->
# we need to fetch the state here. else, the observer is not triggered again because Ember says:
# "if the value has not been used in the meantime, we do not need to trigger additional observer calls"
# (performance optimization!)
@get('state')
@stateChangeEvents++
).observes('state')
myPropChangeEvents: 0
myPropObserver: (->
@myPropChangeEvents++
).observes('myProp.@each')
}
module('Shared/SerializableMixinTest', {
setup: ->
@a = A.create()
@b = A.extend({
stateProperties: ['foo']
foo: 42
}).create()
teardown: ->
@a = null
@b = null
})
test 'by default, serialized state is empty (a)', ->
equal(@a.get('state'), '{}', 'state is correct')
equal(@a.stateChangeEvents, 0, 'state change events are correct')
test 'by default, serialized state is empty (b)', ->
equal(@b.get('state'), '{}', 'state is correct')
equal(@b.stateChangeEvents, 0, 'state change events are correct')
test 'simple properties: writing to a simple property changes the state (b)', ->
@b.set('foo', 1)
deepEqual(JSON.parse(@b.get('state')), {foo: 1}, 'state is correct')
equal(@b.stateChangeEvents, 1, 'state change events are correct')
test 'simple properties: resetting a simple property makes the state empty again (b)', ->
@b.set('foo', 1)
@b.set('foo', 42)
deepEqual(JSON.parse(@b.get('state')), {}, 'state is correct')
equal(@b.stateChangeEvents, 2, 'state change events are correct')
test 'array properties: adding an element to an array poperty changes the state (a)', ->
@a.get('myProp').pushObject('a')
equal(@a.stateChangeEvents, 1, 'state change events are correct')
deepEqual(JSON.parse(@a.get('state')), { myProp: ['x', 'a'] }, 'state is correct')
test 'array properties: replacing an array poperty changes the state (a)', ->
@a.set('myProp', ['a', 'b', 'c'])
@a.get('myProp').pushObject('d')
equal(@a.stateChangeEvents, 2, 'state change events are correct')
deepEqual(JSON.parse(@a.get('state')), { myProp: ['a', 'b', 'c', 'd'] }, 'state is correct')
test 'array properties: resetting an array property makes the state empty again', ->
@a.get('myProp').pushObject('d')
@a.set('myProp', ['x'])
equal(@a.stateChangeEvents, 2, 'state change events are correct')
deepEqual(JSON.parse(@a.get('state')), {}, 'state is correct')
test 'writing state: a state can be read and written again', ->
@b.get('myProp').pushObject('d')
equal(@b.myPropChangeEvents, 1, 'property "myProp" change events are correct (1)')
@b.set('foo', 21)
savedState = @b.get('state')
equal(@b.stateChangeEvents, 2, 'state change events are correct (1)')
@b.set('foo', 1)
@b.get('myProp').pushObject('e')
equal(@b.stateChangeEvents, 4, 'state change events are correct (2)')
equal(@b.myPropChangeEvents, 2, 'property "myProp" change events are correct (2)')
@b.set('state', savedState)
equal(@b.stateChangeEvents, 5, 'state change events are correct (3)')
equal(@b.myPropChangeEvents, 3, 'property "myProp" change events are correct (3)')
equal(@b.get('foo'), 21, 'property "foo" correctly restored')
deepEqual(@b.get('myProp'), ['x', 'd'], 'property "myProp" correctly restored')
test 'writing state: if a property has not changed, the default state is used.', ->
@b.get('myProp').pushObject('d')
savedState = @b.get('state')
@b.set('foo', 1)
@b.set('state', savedState)
equal(@b.get('foo'), 42, 'property "foo" correctly restored to default value')
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment