PageRenderTime 70ms CodeModel.GetById 45ms RepoModel.GetById 0ms app.codeStats 1ms

/README.md

https://github.com/cjroebuck/Backbone-relational
Markdown | 311 lines | 237 code | 74 blank | 0 comment | 0 complexity | 921c84c5903562b6a3b52f7f528fd0c2 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. * Retrieve (a set of) related models through the `fetchRelated(key<string>, [options<object>])` 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. * changes to the key itself on `HasMany` and `HasOne` relations (bind to `update:<key>`; arguments=`(model, relatedModel/relatedCollection)`).
  12. ## Contents
  13. * [Installation](#installation)
  14. * [Backbone.Relation options](#backbone-relation)
  15. * [Backbone.RelationalModel](#backbone-relationalmodel)
  16. * [Example](#example)
  17. * [Under the hood](#under-the-hood)
  18. ## <a name="installation"/>Installation
  19. 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:
  20. ```javascript
  21. <script type="text/javascript" src="./js/underscore.js"></script>
  22. <script type="text/javascript" src="./js/backbone.js"></script>
  23. <script type="text/javascript" src="./js/backbone-relational.js"></script>
  24. ```
  25. Backbone-relational has been tested with Backbone 0.5.1 (or newer) and Underscore 1.1.6 (or newer).
  26. ## <a name="backbone-relation"/>Backbone.Relation options
  27. Each `Backbone.RelationalModel` can contain an array of `relations`. Each relation supports a number of options, of which `relatedModel`, `key` and `type` are mandatory. A relation could look like the following:
  28. ```javascript
  29. Zoo = Backbone.RelationalModel.extend({
  30. relations: [{
  31. type: Backbone.HasMany,
  32. key: 'animals',
  33. relatedModel: 'Animal',
  34. collectionType: 'AnimalCollection',
  35. reverseRelation: {
  36. key: 'livesIn',
  37. includeInJSON: 'id'
  38. // 'relatedModel' is automatically set to 'Zoo'; the 'relationType' to 'HasOne'.
  39. }
  40. }]
  41. });
  42. Animal = Backbone.RelationalModel.extend({
  43. urlRoot: '/animal/'
  44. });
  45. AnimalCollection = Backbone.Collection.extend({
  46. model: Animal,
  47. url: function( models ) {
  48. return '/animal/' + ( models ? 'set/' + _.pluck( models, 'id' ).join(';') + '/' : '' );
  49. }
  50. });
  51. ```
  52. ### relatedModel
  53. Value: a string (which can be resolved to an object type on the global scope), or a reference to a `Backbone.RelationalModel` type.
  54. ### key
  55. Value: a string. References an attribute name on `relatedModel`.
  56. ### type
  57. Value: a string, or a reference to a `Backbone.Relation` type
  58. Example: `Backbone.HasOne` or `'HasMany'`.
  59. #### HasOne relations (`Backbone.HasOne`)
  60. The key for a `HasOne` relation consists of a single `Backbone.RelationalModel`. The default `reverseRelation.type` for a HasOne relation is HasMany. This can be set to `HasOne` instead, to create a one-to-one relation.
  61. #### HasMany relations (`Backbone.HasMany`)
  62. The key for a `HasMany` relation consists of a `Backbone.Collection`, containing zero or more `Backbone.RelationalModel`s. The default `reverseRelation.type` for a HasMany relation is HasOne; this is the only option here, since many-to-many is not supported directly.
  63. #### <a name="many-to-many"/>Many-to-many relations
  64. A many-to-many relation can be modeled using two `Backbone.HasMany` relations, with a link model in between:
  65. ```javascript
  66. Person = Backbone.RelationalModel.extend({
  67. relations: [
  68. {
  69. type: 'HasMany',
  70. key: 'jobs',
  71. relatedModel: 'Job',
  72. reverseRelation: {
  73. key: 'person'
  74. }
  75. }
  76. ]
  77. });
  78. // A link object between 'Person' and 'Company', to achieve many-to-many relations.
  79. Job = Backbone.RelationalModel.extend({
  80. defaults: {
  81. 'startDate': null,
  82. 'endDate': null
  83. }
  84. })
  85. Company = Backbone.RelationalModel.extend({
  86. relations: [
  87. {
  88. type: 'HasMany',
  89. key: 'employees',
  90. relatedModel: 'Job',
  91. reverseRelation: {
  92. key: 'company'
  93. }
  94. }
  95. ]
  96. });
  97. niceCompany = new Company( { name: 'niceCompany' } );
  98. niceCompany.bind( 'add:employees', function( model, coll ) {
  99. // Will see a Job with attributes { person: paul, company: niceCompany } being added here
  100. });
  101. paul.get('jobs').add( { company: niceCompany } );
  102. ```
  103. ### collectionType
  104. Value: a string (which can be resolved to an object type on the global scope), or a reference to a `Backbone.Collection` type.
  105. Determine the type of collections used for a `HasMany` relation. Defining a `url(models<Backbone.Model[]>)` function on this Collection that's able to build a url for either the whole collection, or a set of models enables `fetchRelated` to fetch all missing models in one request, instead of firing a separate request for each. See [Backbone-tastypie](https://github.com/PaulUithol/backbone-tastypie/blob/master/backbone-tastypie.js#L74) for an example.
  106. ### includeInJSON
  107. Value: a boolean, or a string referencing one of the model's attributes. Default: `true`.
  108. Determines how a relation will be serialized following a call to the `toJSON` method. A value of `true` serializes the full set of attributes on the related model(s), in which case the relations of this object are serialized as well. Set to `false` to exclude the relation completely. You can also choose to include a single attribute from the related model by using a string. For example, `'name'`, or `Backbone.Model.prototype.idAttribute` to include ids.
  109. ### createModels
  110. Value: a boolean. Default: `true`.
  111. Should models be created from nested objects, or not?
  112. ### reverseRelation
  113. If the relation should be bidirectional, specify the details for the reverse relation here. 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.
  114. **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.
  115. ## <a name="backbone-relationalmodel"/>Backbone.RelationalModel
  116. `Backbone.RelationalModel` introduces a couple of new methods and events.
  117. ### Methods
  118. #### getRelations `relationalModel.getRelations()`
  119. Returns the set of initialized relations on the model.
  120. #### fetchRelated `relationalModel.fetchRelated(key<string>, [options<object>])`
  121. Fetch models from the server that were referenced in the model's attributes, but have not been found/created yet. This can be used specifically for lazy-loading scenarios.
  122. By default, a separate request will be fired for each additional model that is to be fetched from the server. However, if your server/API supports it, you can fetch the set of models in one request by specifying a `collectionType` for the relation you call `fetchRelated` on. The `collectionType` should have an overridden `url(models<Backbone.Model[]>)` method that allows it to construct a url for an array of models. See the example at the top of [Backbone.Relation options](#backbone-relation) or [Backbone-tastypie](https://github.com/PaulUithol/backbone-tastypie/blob/master/backbone-tastypie.js#L74) for an example.
  123. ### Events
  124. * `add`: triggered on addition to a `HasMany` relation.
  125. Bind to `add:<key>`; arguments: `(addedModel<Backbone.Model>, related<Backbone.Collection>)`.
  126. * `remove`: triggered on removal from a `HasMany` relation.
  127. Bind to `remove:<key>`; arguments: `(removedModel<Backbone.Model>, related<Backbone.Collection>)`.
  128. * `update`: triggered on changes to the key itself on `HasMany` and `HasOne` relations.
  129. Bind to `update:<key>`; arguments: `(model<Backbone.Model>, related<Backbone.Model|Backbone.Collection>)`.
  130. ## <a name="example"/>Example
  131. ```javascript
  132. paul = new Person({
  133. id: 'person-1',
  134. name: 'Paul',
  135. user: { id: 'user-1', login: 'dude', email: 'me@gmail.com' }
  136. });
  137. // A User object is automatically created from the JSON; so 'login' returns 'dude'.
  138. paul.get('user').get('login');
  139. ourHouse = new House({
  140. id: 'house-1',
  141. location: 'in the middle of the street',
  142. occupants: ['person-1', 'person-2', 'person-5']
  143. });
  144. // 'ourHouse.occupants' is turned into a Backbone.Collection of Persons.
  145. // The first person in 'ourHouse.occupants' will point to 'paul'.
  146. ourHouse.get('occupants').at(0); // === paul
  147. // the relation from 'House.occupants' to 'Person' has been defined as a bi-directional HasMany relation,
  148. // with a reverse relation to 'Person.livesIn'. So, 'paul.livesIn' will automatically point back to 'ourHouse'.
  149. paul.get('livesIn'); // === ourHouse
  150. // You can control which relations get serialized to JSON (when saving), using the 'includeInJSON'
  151. // property on a Relation. Also, each object will only get serialized once to prevent loops.
  152. paul.get('user').toJSON();
  153. /* result:
  154. {
  155. email: "me@gmail.com",
  156. id: "user-1",
  157. login: "dude",
  158. person: {
  159. id: "person-1",
  160. name: "Paul",
  161. livesIn: {
  162. id: "house-1",
  163. location: "in the middle of the street",
  164. occupants: ["person-1"] // just the id, since 'includeInJSON' references the 'idAttribute'
  165. },
  166. user: "user-1" // not serialized because it is already in the JSON, so we won't create a loop
  167. }
  168. }
  169. */
  170. // Load occupants 'person-2' and 'person-5', which don't exist yet, from the server
  171. ourHouse.fetchRelated( 'occupants' );
  172. // Use the 'add' and 'remove' events to listen for additions/removals on HasMany relations (like 'House.occupants').
  173. ourHouse.bind( 'add:occupants', function( model, coll ) {
  174. // create a View?
  175. console.debug( 'add %o', model );
  176. });
  177. ourHouse.bind( 'remove:occupants', function( model, coll ) {
  178. // destroy a View?
  179. console.debug( 'remove %o', model );
  180. });
  181. // Use the 'update' event to listen for changes on a HasOne relation (like 'Person.livesIn').
  182. paul.bind( 'update:livesIn', function( model, attr ) {
  183. console.debug( 'update to %o', attr );
  184. });
  185. // Modifying either side of a bi-directional relation updates the other side automatically.
  186. // Make paul homeless; triggers 'remove:occupants' on ourHouse, and 'update:livesIn' on paul
  187. ourHouse.get('occupants').remove( paul.id );
  188. paul.get('livesIn'); // yup; nothing.
  189. // Move back in; triggers 'add:occupants' on ourHouse, and 'update:livesIn' on paul
  190. paul.set( { 'livesIn': 'house-1' } );
  191. ```
  192. This is achieved using the following relations and models:
  193. ```javascript
  194. House = Backbone.RelationalModel.extend({
  195. // The 'relations' property, on the House's prototype. Initialized separately for each instance of House.
  196. // Each relation must define (as a minimum) the 'type', 'key' and 'relatedModel'. Options are
  197. // 'includeInJSON', 'createModels' and 'reverseRelation', which takes the same options as the relation itself.
  198. relations: [
  199. {
  200. type: Backbone.HasMany, // Use the type, or the string 'HasOne' or 'HasMany'.
  201. key: 'occupants',
  202. relatedModel: 'Person',
  203. includeInJSON: Backbone.Model.prototype.idAttribute,
  204. collectionType: 'PersonCollection',
  205. reverseRelation: {
  206. key: 'livesIn'
  207. }
  208. }
  209. ]
  210. });
  211. Person = Backbone.RelationalModel.extend({
  212. relations: [
  213. { // Create a (recursive) one-to-one relationship
  214. type: Backbone.HasOne,
  215. key: 'user',
  216. relatedModel: 'User',
  217. reverseRelation: {
  218. type: Backbone.HasOne,
  219. key: 'person'
  220. }
  221. }
  222. ],
  223. initialize: function() {
  224. // do whatever you want :)
  225. }
  226. });
  227. PersonCollection = Backbone.Collection.extend({
  228. url: function( models ) {
  229. // Logic to create a url for the whole collection, or a set of models.
  230. // See the tests, or Backbone-tastypie, for an example.
  231. return '/person/' + ( models ? 'set/' + _.pluck( models, 'id' ).join(';') + '/' : '' );
  232. }
  233. });
  234. User = Backbone.RelationalModel.extend();
  235. ```
  236. ## <a name="under-the-hood"/>Under the hood
  237. Each `Backbone.RelationalModel` registers itself with `Backbone.Store` upon creation (and is removed from the `Store` when destroyed). When creating or updating an attribute that is a key in a relation, removed related objects are notified of their removal, and new related objects are looked up in the `Store`.