PageRenderTime 90ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 0ms

/app/components/backbone-relational/README.md

https://bitbucket.org/piemonster/dtclient
Markdown | 562 lines | 417 code | 145 blank | 0 comment | 0 complexity | 4b8e5d8858fe1c49c90d2fec6e0be6b3 MD5 | raw file
  1. # Backbone-relational
  2. Backbone-relational provides one-to-one, one-to-many and many-to-one relations between models for [Backbone](https://github.com/documentcloud/backbone). To use relations, extend `Backbone.RelationalModel` (instead of the regular `Backbone.Model`) and define a property `relations`, containing an array of option objects. Each relation must define (as a minimum) the `type`, `key` and `relatedModel`. Available relation types are `Backbone.HasOne` and `Backbone.HasMany`. Backbone-relational features:
  3. * Bidirectional relations, which notify related models of changes through events.
  4. * Control how relations are serialized using the `includeInJSON` option.
  5. * Automatically convert nested objects in a model's attributes into Model instances using the `createModels` option.
  6. * Lazily retrieve (a set of) related models through the `fetchRelated(key<string>, [options<object>], update<bool>)` method.
  7. * Determine the type of `HasMany` collections with `collectionType`.
  8. * Bind new events to a `Backbone.RelationalModel` for:
  9. * addition to a `HasMany` relation (bind to `add:<key>`; arguments: `(addedModel, relatedCollection)`),
  10. * removal from a `HasMany` relation (bind to `remove:<key>`; arguments: `(removedModel, relatedCollection)`),
  11. * reset of a `HasMany` relation (bind to `reset:<key>`; arguments: `(relatedCollection)`),
  12. * changes to the key itself on `HasMany` and `HasOne` relations (bind to `update:<key>`; arguments=`(model, relatedModel/relatedCollection)`).
  13. ## Contents
  14. * [Getting started](#getting-started)
  15. * [Backbone.Relation options](#backbone-relation)
  16. * [Backbone.RelationalModel](#backbone-relationalmodel)
  17. * [Example](#example)
  18. * [Known problems and solutions](#q-and-a)
  19. * [Under the hood](#under-the-hood)
  20. ## <a name="getting-started"/>Getting started
  21. Resources to get you started with Backbone-relational:
  22. * [A great tutorial by antoviaque](http://antoviaque.org/docs/tutorials/backbone-relational-tutorial/) ([and the accompanying git repository](https://github.com/antoviaque/backbone-relational-tutorial))
  23. ### <a name="installation"/>Installation
  24. Backbone-relational depends on [Backbone](https://github.com/documentcloud/backbone) (and thus on [Underscore](https://github.com/documentcloud/underscore)). Include Backbone-relational right after Backbone and Underscore:
  25. ```html
  26. <script type="text/javascript" src="./js/underscore.js"></script>
  27. <script type="text/javascript" src="./js/backbone.js"></script>
  28. <script type="text/javascript" src="./js/backbone-relational.js"></script>
  29. ```
  30. Backbone-relational has been tested with Backbone 0.9.10 (or newer) and Underscore 1.4.3 (or newer).
  31. ## <a name="backbone-relation"/>Backbone.Relation options
  32. Each `Backbone.RelationalModel` can contain an array of `relations`.
  33. Each relation supports a number of options, of which `relatedModel`, `key` and `type` are mandatory.
  34. A relation could look like the following:
  35. ```javascript
  36. Zoo = Backbone.RelationalModel.extend({
  37. relations: [{
  38. type: Backbone.HasMany,
  39. key: 'animals',
  40. relatedModel: 'Animal',
  41. collectionType: 'AnimalCollection',
  42. reverseRelation: {
  43. key: 'livesIn',
  44. includeInJSON: 'id'
  45. // 'relatedModel' is automatically set to 'Zoo'; the 'relationType' to 'HasOne'.
  46. }
  47. }]
  48. });
  49. Animal = Backbone.RelationalModel.extend({
  50. urlRoot: '/animal/'
  51. });
  52. AnimalCollection = Backbone.Collection.extend({
  53. model: Animal,
  54. url: function( models ) {
  55. return '/animal/' + ( models ? 'set/' + _.pluck( models, 'id' ).join(';') + '/' : '' );
  56. }
  57. });
  58. ```
  59. ### relatedModel
  60. Value: a string (which can be resolved to an object type on the global scope), or a reference to a `Backbone.RelationalModel` type.
  61. ### key
  62. Value: a string. References an attribute name on `relatedModel`.
  63. ### type
  64. Value: a string, or a reference to a `Backbone.Relation` type
  65. Example: `Backbone.HasOne` or `'HasMany'`.
  66. ###### **HasOne relations (`Backbone.HasOne`)**
  67. The key for a `HasOne` relation consists of a single `Backbone.RelationalModel`. The default `reverseRelation.type` for a HasOne relation is HasMany.
  68. This can be set to `HasOne` instead, to create a one-to-one relation.
  69. ###### **HasMany relations (`Backbone.HasMany`)**
  70. The key for a `HasMany` relation consists of a `Backbone.Collection`, containing zero or more `Backbone.RelationalModel`s.
  71. The default `reverseRelation.type` for a HasMany relation is HasOne; this is the only option here, since many-to-many is not supported directly.
  72. ###### **<a name="many-to-many"/>Many-to-many relations**
  73. A many-to-many relation can be modeled using two `Backbone.HasMany` relations, with a link model in between:
  74. ```javascript
  75. Person = Backbone.RelationalModel.extend({
  76. relations: [
  77. {
  78. type: 'HasMany',
  79. key: 'jobs',
  80. relatedModel: 'Job',
  81. reverseRelation: {
  82. key: 'person'
  83. }
  84. }
  85. ]
  86. });
  87. // A link object between 'Person' and 'Company', to achieve many-to-many relations.
  88. Job = Backbone.RelationalModel.extend({
  89. defaults: {
  90. 'startDate': null,
  91. 'endDate': null
  92. }
  93. })
  94. Company = Backbone.RelationalModel.extend({
  95. relations: [
  96. {
  97. type: 'HasMany',
  98. key: 'employees',
  99. relatedModel: 'Job',
  100. reverseRelation: {
  101. key: 'company'
  102. }
  103. }
  104. ]
  105. });
  106. niceCompany = new Company( { name: 'niceCompany' } );
  107. niceCompany.bind( 'add:employees', function( model, coll ) {
  108. // Will see a Job with attributes { person: paul, company: niceCompany } being added here
  109. });
  110. paul.get( 'jobs' ).add( { company: niceCompany } );
  111. ```
  112. ### keySource
  113. Value: a string. References an attribute on the data used to instantiate `relatedModel`.
  114. Used to override `key` when determining what data to use when (de)serializing a relation, since the data backing your relations may use different naming conventions.
  115. For example, a Rails backend may provide the keys suffixed with `_id` or `_ids`. The behavior for `keySource` corresponds to the following rules:
  116. 1. When a relation is instantiated, the contents of the `keySource` are used as it's initial data.
  117. 2. The application uses the regular `key` attribute to interface with the relation and the models in it; the `keySource` is not available as an attribute for the model.
  118. So you may be provided with data containing `animal_ids`, while you want to access this relation as `zoo.get( 'animals' );`.
  119. **NOTE**: for backward compatibility reasons, setting `keySource` will set `keyDestination` as well.
  120. This means that when saving `zoo`, the `animals` attribute will be serialized back into the `animal_ids` key.
  121. **WARNING**: when using a `keySource`, you should not use that attribute name for other purposes.
  122. ### keyDestination
  123. Value: a string. References an attribute to serialize `relatedModel` into.
  124. Used to override `key` (and `keySource`) when determining what attribute to be written into when serializing a relation, since the server backing your relations may use different naming conventions.
  125. For example, a Rails backend may expect the keys to be suffixed with `_attributes` for nested attributes.
  126. When calling `toJSON` on a model (either via `Backbone.sync`, or directly), the data in the `key` attribute is transformed and assigned to the `keyDestination`.
  127. So you may want a relation to be serialized into the `animals_attributes` key, while you want to access this relation as `zoo.get( 'animals' );`.
  128. **WARNING**: when using a `keyDestination`, you should not use that attribute name for other purposes.
  129. ### collectionType
  130. Value: a string (which can be resolved to an object type on the global scope), or a reference to a `Backbone.Collection` type.
  131. Determine the type of collections used for a `HasMany` relation. If you define a `url(models<Backbone.Model[]>)` function on
  132. the specified collection, this enables `fetchRelated` to fetch all missing models in one request, instead of firing a separate request for each.
  133. See [Backbone-tastypie](https://github.com/PaulUithol/backbone-tastypie/blob/master/backbone_tastypie/static/js/backbone-tastypie.js#L92) for an example
  134. of a `url` function that can build a url for the collection (or a subset of models).
  135. ### collectionKey
  136. Value: a string or a boolean
  137. Used to create a back reference from the `Backbone.Collection` used for a `HasMany` relation to the model on the other side of this relation.
  138. By default, the relation's `key` attribute will be used to create a reference to the RelationalModel instance from the generated collection.
  139. If you set `collectionKey` to a string, it will use that string as the reference to the RelationalModel, rather than the relation's `key` attribute.
  140. If you don't want this behavior at all, set `collectionKey` to false (or any falsy value) and this reference will not be created.
  141. ### collectionOptions
  142. Value: an options hash or a function that accepts an instance of a `Backbone.RelationalModel` and returns an option hash
  143. Used to provide options for the initialization of the collection in the "Many"-end of a `HasMany` relation. Can be an options hash or
  144. a function that should take the instance in the "One"-end of the "HasMany" relation and return an options hash
  145. ### includeInJSON
  146. Value: a boolean, a string referencing one of the model's attributes, or an array of strings referencing model attributes. Default: `true`.
  147. Determines how the contents of a relation will be serialized following a call to the `toJSON` method. If you specify a:
  148. * Boolean: a value of `true` serializes the full set of attributes on the related model(s).
  149. Set to `false` to exclude the relation completely.
  150. * String: include a single attribute from the related model(s). For example, `'name'`,
  151. or `Backbone.Model.prototype.idAttribute` to include ids.
  152. * String[]: includes the specified attributes from the related model(s).
  153. Only specifying `true` is cascading, meaning the relations of the model will get serialized as well!
  154. ### createModels
  155. Value: a boolean. Default: `true`.
  156. Should models be created from nested objects, or not?
  157. ### reverseRelation
  158. If the relation should be bidirectional, specify the details for the reverse relation here.
  159. It's only mandatory to supply a `key`; `relatedModel` is automatically set. The default `type` for a `reverseRelation` is `HasMany` for a `HasOne` relation (which can be overridden to `HasOne` in order to create a one-to-one relation), and `HasOne` for a `HasMany` relation. In this case, you cannot create a reverseRelation with type `HasMany` as well; please see [Many-to-many relations](#many-to-many) on how to model these type of relations.
  160. **Please note**: if you define a relation (plus a `reverseRelation`) on a model, but never actually create an instance of that model, the model's `constructor` will never run, which means it's `initializeRelations` will never get called, and the reverseRelation will not be initialized either. In that case, you could either define the relation on the opposite model, or define two single relations. See [issue 20](https://github.com/PaulUithol/Backbone-relational/issues/20) for a discussion.
  161. ### autoFetch
  162. Value: a boolean or an Object (see below). Default: `false`.
  163. If this property is set to `true`, when a model is instantiated the related model is automatically fetched using [fetchRelated](#fetchRelated).
  164. The value of the property can also be an object. In that case the related model is automatically fetched and the object is passed
  165. to [fetchRelated](#fetchRelated) as the options parameter.
  166. ```javascript
  167. var Shop = Backbone.RelationalModel.extend({
  168. relations: [{
  169. type: Backbone.HasMany,
  170. key: 'customers',
  171. relatedModel: 'Customer',
  172. autoFetch: true
  173. },{
  174. type: Backbone.HasOne,
  175. key: 'address',
  176. relatedModel: 'Address',
  177. autoFetch: {
  178. success: function(model, response){
  179. //...
  180. },
  181. error: function(model, response){
  182. //...
  183. }
  184. }
  185. }
  186. ]
  187. ```
  188. ## <a name="backbone-relationalmodel"/>Backbone.RelationalModel
  189. `Backbone.RelationalModel` introduces a couple of new methods, events and properties.
  190. ### Methods
  191. ###### **getRelations `relationalModel.getRelations()`**
  192. Returns the set of initialized relations on the model.
  193. ###### <a name="fetchRelated"/>**fetchRelated `relationalModel.fetchRelated(key<string>, [options<object>], [update<boolean>])`**
  194. Fetch models from the server that were referenced in the model's attributes, but have not been found/created yet.
  195. This can be used specifically for lazy-loading scenarios. Setting `update` to true guarantees that the model
  196. will be fetched from the server and any model that already exists in the store will be updated with the retrieved data.
  197. The options object specifies options to be passed to [Backbone.sync](http://backbonejs.org/#Sync).
  198. By default, a separate request will be fired for each additional model that is to be fetched from the server.
  199. However, if your server/API supports it, you can fetch the set of models in one request by specifying a `collectionType`
  200. for the relation you call `fetchRelated` on. The `collectionType` should have an overridden `url(models<Backbone.Model[]>)`
  201. method that allows it to construct a url for an array of models.
  202. See the example at the top of [Backbone.Relation options](#backbone-relation) or
  203. [Backbone-tastypie](https://github.com/PaulUithol/backbone-tastypie/blob/master/backbone_tastypie/static/js/backbone-tastypie.js#L92) for an example.
  204. ### Methods on the type itself
  205. Several methods don't operate on model instances, but are defined on the type itself.
  206. ###### **setup `ModelType.setup()`**
  207. Initialize the relations and submodels for the model type. See the [`Q and A`](#q-and-a) for a possible scenario where
  208. it's useful to call this method manually.
  209. ###### **build `ModelType.build(attributes<object>, [options<object>])`**
  210. Create an instance of a model, taking into account what submodels have been defined.
  211. ###### **findOrCreate `ModelType.findOrCreate(attributes<string|number|object>, [options<object>])`**
  212. Search for a model instance in the `Backbone.Relational.store`.
  213. * If `attributes` is a string or a number, `findOrCreate` will just query the `store` and return a model if found.
  214. * If `attributes` is an object, the model will be updated with `attributes` if found.
  215. Otherwise, a new model is created with `attributes` (unless `options.create` is explicitly set to `false`).
  216. ### Events
  217. * `add`: triggered on addition to a `HasMany` relation.
  218. Bind to `add:<key>`; arguments: `(addedModel<Backbone.Model>, related<Backbone.Collection>)`.
  219. * `remove`: triggered on removal from a `HasMany` relation.
  220. Bind to `remove:<key>`; arguments: `(removedModel<Backbone.Model>, related<Backbone.Collection>)`.
  221. * `update`: triggered on changes to the key itself on `HasMany` and `HasOne` relations.
  222. Bind to `update:<key>`; arguments: `(model<Backbone.Model>, related<Backbone.Model|Backbone.Collection>)`.
  223. ### Properties
  224. Properties can be defined along with the subclass prototype when extending `Backbone.RelationalModel` or a subclass thereof.
  225. ###### <a name="property-submodel-types" />**subModelTypes**
  226. Value: an object. Default: `{}`.
  227. A mapping that defines what submodels exist for the model (the `superModel`) on which `subModelTypes` is defined.
  228. The keys are used to match the [`subModelTypeAttribute`](#property-submodel-type-attribute) when deserializing,
  229. and the values determine what type of submodel should be created for a key. When building model instances from data,
  230. we need to determine what kind of object we're dealing with in order to create instances of the right `subModel` type.
  231. This is done by finding the model for which the key is equal to the value of the
  232. [`submodelTypeAttribute`](#property-submodel-type-attribute) attribute on the passed in data.
  233. Each `subModel` is considered to be a proper submodel of its superclass (the model type you're extending),
  234. with a shared id pool. This means that when looking for an object of the supermodel's type, objects
  235. of a submodel's type can be returned as well, as long as the id matches. In effect, any relations pointing to
  236. the supermodel will look for instances of it's submodels as well.
  237. Example:
  238. ```javascript
  239. Mammal = Animal.extend({
  240. subModelTypes: {
  241. 'primate': 'Primate',
  242. 'carnivore': 'Carnivore'
  243. }
  244. });
  245. var Primate = Mammal.extend();
  246. var Carnivore = Mammal.extend();
  247. var MammalCollection = AnimalCollection.extend({
  248. model: Mammal
  249. });
  250. // Create a collection that contains a 'Primate' and a 'Carnivore'.
  251. var mammals = new MammalCollection([
  252. { id: 3, species: 'chimp', type: 'primate' },
  253. { id: 5, species: 'panther', type: 'carnivore' }
  254. ]);
  255. ```
  256. Suppose that we have an `Mammal` model and a `Primate` model extending `Mammal`. If we have a `Primate` object with
  257. id `3`, this object will be returned when we have a relation pointing to a `Mammal` with id `3`, as `Primate` is
  258. regarded a specific kind of `Mammal`; it's just a `Mammal` with possibly some primate-specific properties or methods.
  259. Note that this means that there cannot be any overlap in ids between instances of `Mammal` and `Primate`, as the
  260. `Primate` with id `3` will *be* the `Mammal` with id `3`.
  261. ###### <a name="property-submodel-type-attribute" />**subModelTypeAttribute**
  262. Value: a string. Default: `"type"`.
  263. The `subModelTypeAttribute` is a references an attribute on the data used to instantiate `relatedModel`.
  264. The attribute that will be checked to determine the type of model that
  265. should be built when a raw object of attributes is set as the related value,
  266. and if the `relatedModel` has one or more submodels.
  267. See [`subModelTypes`](#property-submodel-types) for more information.
  268. ## <a name="example"/>Example
  269. ```javascript
  270. paul = new Person({
  271. id: 'person-1',
  272. name: 'Paul',
  273. user: { id: 'user-1', login: 'dude', email: 'me@gmail.com' }
  274. });
  275. // A User object is automatically created from the JSON; so 'login' returns 'dude'.
  276. paul.get('user').get('login');
  277. ourHouse = new House({
  278. id: 'house-1',
  279. location: 'in the middle of the street',
  280. occupants: ['person-1', 'person-2', 'person-5']
  281. });
  282. // 'ourHouse.occupants' is turned into a Backbone.Collection of Persons.
  283. // The first person in 'ourHouse.occupants' will point to 'paul'.
  284. ourHouse.get('occupants').at(0); // === paul
  285. // If a collection is created from a HasMany relation, it contains a reference
  286. // back to the originator of the relation
  287. ourHouse.get('occupants').livesIn; // === ourHouse
  288. // the relation from 'House.occupants' to 'Person' has been defined as a bi-directional HasMany relation,
  289. // with a reverse relation to 'Person.livesIn'. So, 'paul.livesIn' will automatically point back to 'ourHouse'.
  290. paul.get('livesIn'); // === ourHouse
  291. // You can control which relations get serialized to JSON (when saving), using the 'includeInJSON'
  292. // property on a Relation. Also, each object will only get serialized once to prevent loops.
  293. paul.get('user').toJSON();
  294. /* result:
  295. {
  296. email: "me@gmail.com",
  297. id: "user-1",
  298. login: "dude",
  299. person: {
  300. id: "person-1",
  301. name: "Paul",
  302. livesIn: {
  303. id: "house-1",
  304. location: "in the middle of the street",
  305. occupants: ["person-1"] // just the id, since 'includeInJSON' references the 'idAttribute'
  306. },
  307. user: "user-1" // not serialized because it is already in the JSON, so we won't create a loop
  308. }
  309. }
  310. */
  311. // Load occupants 'person-2' and 'person-5', which don't exist yet, from the server
  312. ourHouse.fetchRelated( 'occupants' );
  313. // Use the 'add' and 'remove' events to listen for additions/removals on HasMany relations (like 'House.occupants').
  314. ourHouse.bind( 'add:occupants', function( model, coll ) {
  315. // create a View?
  316. console.debug( 'add %o', model );
  317. });
  318. ourHouse.bind( 'remove:occupants', function( model, coll ) {
  319. // destroy a View?
  320. console.debug( 'remove %o', model );
  321. });
  322. // Use the 'update' event to listen for changes on a HasOne relation (like 'Person.livesIn').
  323. paul.bind( 'update:livesIn', function( model, attr ) {
  324. console.debug( 'update to %o', attr );
  325. });
  326. // Modifying either side of a bi-directional relation updates the other side automatically.
  327. // Make paul homeless; triggers 'remove:occupants' on ourHouse, and 'update:livesIn' on paul
  328. ourHouse.get('occupants').remove( paul.id );
  329. paul.get('livesIn'); // yup; nothing.
  330. // Move back in; triggers 'add:occupants' on ourHouse, and 'update:livesIn' on paul
  331. paul.set( { 'livesIn': 'house-1' } );
  332. ```
  333. This is achieved using the following relations and models:
  334. ```javascript
  335. House = Backbone.RelationalModel.extend({
  336. // The 'relations' property, on the House's prototype. Initialized separately for each instance of House.
  337. // Each relation must define (as a minimum) the 'type', 'key' and 'relatedModel'. Options are
  338. // 'includeInJSON', 'createModels' and 'reverseRelation', which takes the same options as the relation itself.
  339. relations: [
  340. {
  341. type: Backbone.HasMany, // Use the type, or the string 'HasOne' or 'HasMany'.
  342. key: 'occupants',
  343. relatedModel: 'Person',
  344. includeInJSON: Backbone.Model.prototype.idAttribute,
  345. collectionType: 'PersonCollection',
  346. reverseRelation: {
  347. key: 'livesIn'
  348. }
  349. }
  350. ]
  351. });
  352. Person = Backbone.RelationalModel.extend({
  353. relations: [
  354. { // Create a (recursive) one-to-one relationship
  355. type: Backbone.HasOne,
  356. key: 'user',
  357. relatedModel: 'User',
  358. reverseRelation: {
  359. type: Backbone.HasOne,
  360. key: 'person'
  361. }
  362. }
  363. ],
  364. initialize: function() {
  365. // do whatever you want :)
  366. }
  367. });
  368. PersonCollection = Backbone.Collection.extend({
  369. url: function( models ) {
  370. // Logic to create a url for the whole collection, or a set of models.
  371. // See the tests, or Backbone-tastypie, for an example.
  372. return '/person/' + ( models ? 'set/' + _.pluck( models, 'id' ).join(';') + '/' : '' );
  373. }
  374. });
  375. User = Backbone.RelationalModel.extend();
  376. ```
  377. ## <a name="q-and-a"/>Known problems and solutions
  378. > **Q:** (Reverse) relations or submodels don't seem to be initialized properly (and I'm using CoffeeScript!)
  379. **A:** You're probably using the syntax `class MyModel extends Backbone.RelationalModel` instead of `MyModel = Backbone.RelationalModel.extend`.
  380. This has advantages in CoffeeScript, but it also means that `Backbone.Model.extend` will not get called.
  381. Instead, CoffeeScript generates piece of code that would normally achieve roughly the same.
  382. However, `extend` is also the method that Backbone-relational overrides to set up relations and other things as you're defining your `Backbone.RelationalModel` subclass.
  383. For exactly this scenario where you're not using `.extend`, `Backbone.RelationalModel` has the `.setup` method, that you can call manually after defining your subclass CoffeeScript-style. For example:
  384. ```javascript
  385. class MyModel extends Backbone.RelationalModel
  386. relations: [
  387. // etc
  388. ]
  389. MyModel.setup()
  390. ```
  391. See [issue #91](https://github.com/PaulUithol/Backbone-relational/issues/91) for more information.
  392. > **Q:** After a fetch, I don't get `add:<key>` events for nested relations.
  393. **A:** This is due to `Backbone.Collection.reset` silencing add events. Pass `fetch( {add: true} )` to bypass this problem.
  394. You may want to override `Backbone.Collection.fetch` for this, and also trigger an event when the fetch has finished while you're at it.
  395. Example:
  396. ```javascript
  397. var _fetch = Backbone.Collection.prototype.fetch;
  398. Backbone.Collection.prototype.fetch = function( options ) {
  399. options || ( options = {} );
  400. _.defaults( options, { add: true } );
  401. // Remove old models
  402. this.reset();
  403. // Call 'fetch', and trigger an event when done.
  404. var dit = this,
  405. request = _fetch.call( this, options );
  406. request.done( function() {
  407. if ( !options.silent ) {
  408. dit.trigger( 'fetch', dit, options );
  409. }
  410. });
  411. return request;
  412. };
  413. ```
  414. ## <a name="under-the-hood"/>Under the hood
  415. Each `Backbone.RelationalModel` registers itself with `Backbone.Store` upon creation (and is removed from the `Store` when destroyed).
  416. When creating or updating an attribute that is a key in a relation, removed related objects are notified of their removal,
  417. and new related objects are looked up in the `Store`.