Created
April 30, 2016 02:03
-
-
Save noahmiller/d5188323a011dadcc9ef935c39a483bd to your computer and use it in GitHub Desktop.
Backbone.Model Properties by Configuration
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
/** | |
* A model base class that extends Backbone.Model to provide both | |
* common functionality and common model properties. | |
* | |
* Common properties are defined by property configuration data objects returned | |
* by the propConfig getter. | |
* | |
* Common functionality determined by propConfig includes: | |
* - defaults | |
* - server data parsing | |
*/ | |
class ABaseModel extends Backbone.Model { | |
/** | |
* Returns an array of objects that are used to generate properties and | |
* default values for the model class. Subclasses that override this should | |
* concat additional prop objects to the results of super, e.g.: | |
* return super.propConfig.concat([{...}]); | |
* so that properties defined in ABaseModel or other parent classes aren't lost. | |
* | |
* Each prop object can have the following values: | |
* - prop {string}: the name of the property to be defined. | |
* Required. | |
* - attr {string}: the name of the Backbone model attribute. Useful if | |
* the server returns an attribute like `_xzserver_full_name` that would | |
* read better in the frontend as `fullName`. | |
* Defaults to `prop` value. | |
* - get {func}: the getter function. | |
* Defaults to `Backbone.Model.get(attr)`. | |
* - set {func}: the setter function. If this is present but undefined, | |
* no setter will be created for the property, resulting in a read-only prop. | |
* Defaults to `Backbone.Model.set(attr, value)` | |
* - configurable {bool}: `true` if and only if the type of this property | |
* descriptor may be changed and if the property may be deleted from | |
* the corresponding object. | |
* Defaults to `false`. | |
* - enumerable {bool}: `true` if and only if this property shows up during | |
* enumeration of the properties on the corresponding object. | |
* Defaults to `true`. | |
* | |
* - default {* | func}: default value for the property, as returned by | |
* Backbone.Model.defaults. If a function, called with `this` set to | |
* the model instance. A function should be used to return all | |
* object defaults (i.e. non-primitives), e.g.: | |
* {prop: 'data', default: function() {return {};}} | |
* since otherwise all model instances would share the same single object. | |
* | |
* - mapping {string}: if set, `parse` will map server values to frontend values. | |
* Options: | |
* 'date': passes the value (which should be a string in a format | |
* recognized by the `Date.parse()`` method) to a new `Date` constructor, | |
* and stores the date object in the model instance. | |
* 'relation': passes the value (which should be an object or an array | |
* of objects with model attributes) to a model constructor, | |
* as specified by the `class` property, and stores the new | |
* model instance or a model collection in the owning model instance. | |
* Uses a new Backbone.Collection if the server value is an array. | |
* - class {class}: the class to construct for relation mappings. | |
* Required if mapping == 'relation'. | |
* | |
* Examples: | |
* | |
* // Simple prop | |
* {prop: 'name'} | |
* | |
* // Prop with different server attribute name | |
* {prop: 'firstName', attr: 'first_name'} | |
* | |
* // Un-modifiable prop | |
* {prop: 'emailConfirmed', set: undefined} | |
* | |
* // Un-enumerable prop | |
* {prop: 'syncId', enumerable: false} | |
* | |
* // Primitive default value | |
* {prop: 'active', default: true} | |
* | |
* // Object default value | |
* {prop: 'extraData', default: function() { return {}; }} | |
* | |
* // Date default value | |
* {prop: 'created', default: function() { return new Date(); }} | |
* | |
* // Uses date parsing | |
* {prop: 'lastModified', mapping: 'date'} | |
* | |
* // Uses relationship parsing | |
* {prop: 'address', mapping: 'relation', class: AAddress} | |
* | |
* @returns {array} | |
*/ | |
static get propConfig() { | |
return []; | |
} | |
/** | |
* Configures properties on the model class based on the results of propConfig. | |
* Should be called on each subclass of ABaseModel. | |
*/ | |
static configureProps() { | |
for (const propData of this.propConfig) { | |
const prop = propData.prop; | |
// attr is optional, defaults to prop | |
const attr = propData.attr || propData.prop; | |
const descriptor = propData; | |
Object.defineProperty(this.prototype, prop, Object.assign({ | |
get: function() { | |
return this.get(attr); | |
}, | |
set: function(value) { | |
this.set(attr, value); | |
}, | |
enumerable: true, | |
}, descriptor)); | |
} | |
} | |
/** | |
* `Backbone.Model.defaults` override gets defaults from propConfig. | |
* Called automatically by Backbone.Model.constructor. | |
*/ | |
get defaults() { | |
const defaults = {}; | |
this.constructor.propConfig.forEach((propData) => { | |
const attr = propData.attr || propData.prop; | |
let def = propData.default; | |
if ('function' === typeof def) | |
{ | |
def = propData.default.call(this) | |
} | |
defaults[attr] = def; | |
}) | |
return defaults; | |
} | |
/** | |
* Returns `propConfig` array, filtered by properties that have a `mapping` value. | |
* @returns {array} | |
*/ | |
get mappings() { | |
return this.constructor.propConfig.filter((propData) => { | |
return !!propData.mapping; | |
}); | |
} | |
/** | |
* Override `Backbone.Model.parse` to map API values to model values based on | |
* propConfig. `parse` is called automatically whenever a model's data is | |
* returned by the server, in `fetch`, and `save`. | |
*/ | |
parse(response) { | |
for (const propData of this.mappings) { | |
const attr = propData.attr || propData.prop; | |
const mapping = propData.mapping; | |
const val = response[attr]; | |
if (null !== val && undefined !== val) { | |
switch (mapping) { | |
case 'date': | |
response[attr] = new Date(val); | |
break; | |
case 'relation': | |
if (val instanceof Array) | |
{ | |
const collection = new Backbone.Collection(); | |
collection.model = propData.class; | |
collection.add(val, {parse: true}); | |
response[attr] = collection; | |
} | |
else | |
{ | |
response[attr] = new propData.class(val, {parse: true}); | |
} | |
break; | |
default: | |
throw new Error(`Unknown mapping type ${mapping} for prop ${attr}`); | |
} | |
} | |
} | |
return response; | |
} | |
} |
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
// Create some test data | |
window.fred = new AContact({ | |
id: 1, | |
first_name: 'Fred', | |
last_name: 'Barns', | |
email: 'fbarns@foo.com', | |
home: { | |
id: 1, | |
address: '55 Lux Ave', | |
city: 'Asheville', | |
state: 'NC', | |
}, | |
offices: [ | |
{ | |
id: 2, | |
address: '1 State St', | |
city: 'NYC', | |
state: 'NY', | |
}, | |
{ | |
id: 3, | |
address: '50 Birch St', | |
city: 'Portland', | |
state: 'OR', | |
}, | |
], | |
}, {parse: true}); | |
window.erin = new AContact({ | |
id: 2, | |
first_name: 'Erin', | |
last_name: 'Birch', | |
active: false, | |
created_at: 'Jan 1 1980 12:00:00 GMT-0400', | |
}, {parse: true}); | |
window.home = new AAddress({ | |
id: 4, | |
address: '123 Main St.', | |
city: 'Burlington', | |
state: 'VT', | |
}, {parse: true}); |
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
<!doctype html> | |
<head> | |
<meta charset="utf-8"> | |
<title>Backbone.Model Properties by Configuration</title> | |
<meta name="viewport" content="initial-scale=1.0,user-scalable=no,maximum-scale=1,width=device-width"> | |
</head> | |
<body> | |
<header class="page-header"> | |
<div class="container"> | |
<h1>Backbone.Model Properties by Configuration</h1> | |
<p>Example of creating Backbone.Model properties through configuration.</p> | |
</div> | |
</header> | |
<div> | |
<div> | |
<h2>Fred</h2> | |
<p>Example object created from a contact model that uses prop configuration:</p> | |
<pre> | |
window.fred = new AContact({ | |
id: 1, | |
first_name: 'Fred', | |
last_name: 'Barns', | |
email: 'fbarns@foo.com', | |
home: { | |
id: 1, | |
address: '55 Lux Ave', | |
city: 'Asheville', | |
state: 'NC', | |
}, | |
offices: [ | |
{ | |
id: 2, | |
address: '1 State St', | |
city: 'NYC', | |
state: 'NY', | |
}, | |
], | |
}, {parse: true}); | |
</pre> | |
<p>The prop config parses, sets defaults, and creates properties:</p> | |
<pre> | |
fred.firstName | |
> "Fred" | |
fred.createdAt | |
> Tue Apr 26 2016 14:17:16 GMT-0400 (EDT) | |
fred.home.address | |
> "55 Lux Ave" | |
fred.offices.get(2) | |
> AAddress {cid: "c3", attributes...} | |
</pre> | |
</div> | |
</div> | |
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script> | |
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.3.3/backbone-min.js"></script> | |
<script src="baseModel.js"></script> | |
<script src="models.js"></script> | |
<script src="data.js"></script> | |
</body> |
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
/** | |
* A common model superclass defines properties that all models should have. | |
*/ | |
class ACommonModel extends ABaseModel { | |
static get propConfig() { | |
return super.propConfig.concat([ | |
// Property used in server data parsing and defaults | |
{ | |
prop: 'createdAt', | |
attr: 'created_at', | |
mapping: 'date', | |
default: function() {return new Date();}, | |
}, | |
]); | |
} | |
} | |
ACommonModel.configureProps(); | |
/** | |
* An address model has properties for a street address. | |
*/ | |
class AAddress extends ACommonModel { | |
static get propConfig() { | |
return super.propConfig.concat([ | |
{prop: 'address'}, | |
{prop: 'city'}, | |
{prop: 'state'}, | |
]); | |
} | |
} | |
AAddress.configureProps(); | |
/** | |
* A contact model has properties for a personal contact, including | |
* one-to-one and one-to-many address relations. | |
*/ | |
class AContact extends ACommonModel { | |
static get propConfig() { | |
return super.propConfig.concat([ | |
// Properties with different server attribute names | |
{prop: 'firstName', attr: 'first_name'}, | |
{prop: 'lastName', attr: 'last_name'}, | |
// Property that can't be modified | |
{prop: 'email', set: undefined}, | |
// Property that isn't enumerable, e.g. using for (const prop in model) | |
{prop: 'syncId', enumerable: false}, | |
// Property with a primitive default | |
{prop: 'active', default: true}, | |
// Property with an object default | |
{prop: 'data', default: function() { return {};}}, | |
// To-one relation property, where a model instance is created | |
{prop: 'home', mapping: 'relation', class: AAddress}, | |
// To-many relation property, where a Backbone.Collection is created | |
{prop: 'offices', mapping: 'relation', class: AAddress}, | |
]); | |
} | |
} | |
AContact.configureProps(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment