Created
January 5, 2014 11:42
-
-
Save skurfuerst/8267335 to your computer and use it in GitHub Desktop.
SerializableMixin for Ember.JS
This file contains 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
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 |
This file contains 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
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