diff --git a/docs/framework/backbone.md b/docs/framework/backbone.md index f60bbd58e4dc269c4ea6ec95ee0c33bd81ea0c71..0a9dfe9a3e291267562ea6918a25e5a3efaf56cd 100644 --- a/docs/framework/backbone.md +++ b/docs/framework/backbone.md @@ -1,53 +1,7 @@ # Backbone Reference -<div class="panelMacro"> - -+--+ -| | -+--+ - -</div> - - - -<span id="BackboneReference-status" -class="confluence-anchor-link"></span> - -<div class="panel" -style="background-color: #FFFFCE;border-color: #000;border-style: solid;border-width: 1px;"> - -<div class="panelHeader" -style="border-bottom-width: 1px;border-bottom-style: solid;border-bottom-color: #000;background-color: #F7D6C1;"> - -**Table of Contents** - -</div> - -<div class="panelContent" style="background-color: #FFFFCE;"> - -<div> - -- [Background](#BackboneReference-Background) -- [Examples](#BackboneReference-Examples) -- [External Packages](#BackboneReference-ExternalPackages) -- [CiviCRM Additions](#BackboneReference-CiviCRMAdditions) - -<!-- --> - -- [CRM.Backbone.trackSaved](#BackboneReference-CRM.Backbone.trackSaved) -- [CRM.Backbone.trackSoftDelete](#BackboneReference-CRM.Backbone.trackSoftDelete) -- [CRM.Backbone.sync](#BackboneReference-CRM.Backbone.sync) -- [CRM.Backbone.extend(Model,Collection)](#BackboneReference-CRM.Backbone.extend(Model,Collection)) - -<!-- --> - -- [Unit Tests](#BackboneReference-UnitTests) - -</div> - -</div> - -</div> +!!! failure "Deprecated" + CiviCRM no longer recommends using Backbone. This page is here primarily for archival purposes. ## Background @@ -71,149 +25,71 @@ Backbone is currently used in the following parts of CiviCRM: ## External Packages +* [Underscore](http://documentcloud.github.io/underscore/) + * General utilities for working with objects, arrays, and client-side HTML templates + * Scope of usage: + * Profile Designer + * CiviVolunteer + * CiviHR + +* [Backbone](http://documentcloud.github.io/backbone/) + * MV* framework. Defines three key base-classes: + * `Backbone.Model` - (for representing individual data records) + * `Backbone.Collection` - (for representing a collection of data records) + * `Backbone.View` - (for rendering markup and responding to events) + * Scope of usage: + * Profile Designer + * CiviVolunteer + * CiviHR + +* [Backbone.Marionette](http://marionettejs.com/) + * An "opinionated" Backbone framework. It adds more base-classes which significantly reduce the boiler-plate and clutter required for defining & combining normal `Backbone.View` classes. + * See also: + * [A simple Backbone.Marionette tutorial](http://davidsulc.com/blog/2012/04/15/a-simple-backbone-marionette-tutorial/) (Blog, Apr 2012) + * [Tutorial: A full Backbone.Marionette application](http://davidsulc.com/blog/2012/05/06/tutorial-a-full-backbone-marionette-application-part-1/) (Blog, May 2012) + * [Backbone.Marionette.js: A Simple Introduction](https://leanpub.com/marionette-gentle-introduction) (eBook, July 2013) + * Scope of usage: + * Profile Designer + * CiviVolunteer + * CiviHR + +* [Backbone.ModelBinder](https://github.com/theironcook/Backbone.ModelBinder) + * A two-way link between Backbone "models" and HTML "forms" – form fields can be initialized using data from models, and models can be updated using form fields. + * Scope of usage: + * CiviHR + +* [Backbone.Forms](https://github.com/powmedia/backbone-forms) + * Like `Backbone.ModelBinder`, this can define a two-way link between Backbone "models" and HTML "forms" – however, it goes a step further by auto-generating the HTML form based on the "model schema". + * Scope of usage: + * Profile Designer -<div class="table-wrap"> - -+--------------------------+--------------------------+--------------------------+ -| Package | Description | Scope of Usage | -+==========================+==========================+==========================+ -| [Underscore](http://docu | General utilities for | Profile Designer | -| mentcloud.github.io/unde | working with objects, | | -| rscore/){.external-link} | arrays, and client-side | CiviVolunteer | -| | HTML templates | | -| | | CiviHR | -+--------------------------+--------------------------+--------------------------+ -| [Backbone](http://docume | MV* framework. Defines | Profile Designer | -| ntcloud.github.io/backbo | three key base-classes: | | -| ne/){.external-link} | | CiviVolunteer | -| | - Backbone.Model (for | | -| | representing | CiviHR | -| | individual | | -| | data records) | | -| | - Backbone.Collection | | -| | (for representing a | | -| | collection of | | -| | data records) | | -| | - Backbone.View (for | | -| | rendering markup and | | -| | responding | | -| | to events) | | -+--------------------------+--------------------------+--------------------------+ -| [Backbone.Marionette](ht | An "opinionated" | Profile Designer | -| tp://marionettejs.com/){ | Backbone framework. It | | -| .external-link} | adds more base-classes | CiviVolunteer | -| | which significantly | | -| | reduce the boiler-plate | CiviHR | -| | and clutter required for | | -| | defining & combining | | -| | normal Backbone.View | | -| | classes. | | -| | | | -| | See also: | | -| | | | -| | - [A simple | | -| | Backbone.Marionette | | -| | tutorial (Blog, | | -| | Apr 2012)](http://da | | -| | vidsulc.com/blog/2012/04 | | -| | /15/a-simple-backbone-ma | | -| | rionette-tutorial/){.ext | | -| | ernal-link} | | -| | - [Tutorial: A full | | -| | Backbone.Marionette | | -| | application (Blog, | | -| | May 2012)](http://da | | -| | vidsulc.com/blog/2012/05 | | -| | /06/tutorial-a-full-back | | -| | bone-marionette-applicat | | -| | ion-part-1/){.external-l | | -| | ink} | | -| | - [Backbone.Marionette | | -| | .js: | | -| | A Simple | | -| | Introduction (eBook, | | -| | July 2013)](https:// | | -| | leanpub.com/marionette-g | | -| | entle-introduction){.ext | | -| | ernal-link} | | -+--------------------------+--------------------------+--------------------------+ -| [Backbone.ModelBinder](h | A two-way link between | CiviHR | -| ttps://github.com/theiro | Backbone "models" and | | -| ncook/Backbone.ModelBind | HTML "forms" – form | | -| er){.external-link} | fields can be | | -| | initialized using data | | -| | from models, and models | | -| | can be updated using | | -| | form fields. | | -+--------------------------+--------------------------+--------------------------+ -| [Backbone.Forms](https:/ | Like | Profile Designer | -| /github.com/powmedia/bac | Backbone.ModelBinder, | | -| kbone-forms){.external-l | this can define a | | -| ink} | two-way link between | | -| | Backbone "models" and | | -| | HTML "forms" – however, | | -| | it goes a step further | | -| | by auto-generating the | | -| | HTML form based on the | | -| | "model schema". | | -+--------------------------+--------------------------+--------------------------+ - -</div> - ## CiviCRM Additions -#### CRM.Backbone.trackSaved - -<div class="code panel" style="border-width: 1px;"> - -<div class="codeHeader panelHeader" style="border-bottom-width: 1px;"> - -**Tracking saved/unsaved status** - -</div> - -<div class="codeContent panelContent"> - - // Setup model class - var MyModel = Backbone.Model.extend({...}); - CRM.Backbone.trackSaved(MyModel); - - - // Use the class - var model = new MyModel({id: 123}); - model.fetch(); - // assert: model.isSaved() === true -- because our client matches server - model.set('property', 'value'): - // assert: model.isSaved() === false -- because our client deviates from server - // event: saved(model,is_saved) - model.save(); - // assert: model.isSaved() === true -- because our client matches server - // event: saved(model,is_saved) - -</div> - -</div> - -Note: The fetch() and save() methods each trigger an AJAX call – -***after completing*** the AJAX call, the save-status will be updated. -If you want to update a view based on the save-status, it's best to -define a callback for the model's "saved" event. However, you *can* -update the view using "success", "error", or "sync" callbacks – but you -***must*** use -[_.defer()](http://documentcloud.github.io/underscore/#defer){.external-link} -before checking isSaved(): - -<div class="code panel" style="border-width: 1px;"> - -<div class="codeHeader panelHeader" style="border-bottom-width: 1px;"> - -**Using isSaved() with success & error** - -</div> - -<div class="codeContent panelContent"> - +### `CRM.Backbone.trackSaved` + +Tracking saved/unsaved status + +```javascript +// Setup model class +var MyModel = Backbone.Model.extend({...}); +CRM.Backbone.trackSaved(MyModel); +// Use the class +var model = new MyModel({id: 123}); +model.fetch(); +// assert: model.isSaved() === true -- because our client matches server +model.set('property', 'value'): +// assert: model.isSaved() === false -- because our client deviates from server +// event: saved(model,is_saved) +model.save(); +// assert: model.isSaved() === true -- because our client matches server +// event: saved(model,is_saved) +``` + +!!! note + The `fetch()` and `save()` methods each trigger an AJAX call – ***after completing*** the AJAX call, the save-status will be updated. If you want to update a view based on the save-status, it's best to define a callback for the model's "saved" event. However, you *can* update the view using "success", "error", or "sync" callbacks – but you ***must*** use [`_.defer()`](http://documentcloud.github.io/underscore/#defer) before checking `isSaved()`. For example: + + ```javascript var model = new MyModel({ property1: value1, property2: value2, @@ -233,55 +109,37 @@ before checking isSaved(): }); } }); + ``` -</div> - -</div> - -#### CRM.Backbone.trackSoftDelete - -<div class="code panel" style="border-width: 1px;"> - -<div class="codeHeader panelHeader" style="border-bottom-width: 1px;"> - -**Using soft deletion** - -</div> - -<div class="codeContent panelContent"> - - var MyModel = Backbone.Model.extend({...}); - CRM.Backbone.trackSoftDelete(MyModel); - - // Create an example model - var model = new MyModel({id: 123}); - // assert: model.isSoftDeleted() === false - - - // Flag a model for deletion - model.setSoftDeleted(true); - // assert: model.isSoftDeleted() === true - // event: softDeleted(model, is_soft_deleted) +### `CRM.Backbone.trackSoftDelete` +Using soft deletion - // Remove a deletion flag - model.setSoftDeleted(false); - // assert: model.isSoftDeleted() === false - // event: softDeleted(model, is_soft_deleted) +```javascript +var MyModel = Backbone.Model.extend({...}); +CRM.Backbone.trackSoftDelete(MyModel); +// Create an example model +var model = new MyModel({id: 123}); +// assert: model.isSoftDeleted() === false - // Perform save or deletion (depending on whether isSoftDeleted()) - model.save(); - // If isSoftDeleted()==false, call normal save() - // If isSoftDeleted()==true, call destroy() instead +// Flag a model for deletion +model.setSoftDeleted(true); +// assert: model.isSoftDeleted() === true +// event: softDeleted(model, is_soft_deleted) -</div> +// Remove a deletion flag +model.setSoftDeleted(false); +// assert: model.isSoftDeleted() === false +// event: softDeleted(model, is_soft_deleted) -</div> +// Perform save or deletion (depending on whether isSoftDeleted()) +model.save(); +// If isSoftDeleted()==false, call normal save() +// If isSoftDeleted()==true, call destroy() instead +``` - - -#### CRM.Backbone.sync +### `CRM.Backbone.sync` The Backbone.sync framework is generally used for loading and saving data through web-services. The default implementation of Backbone.sync @@ -300,125 +158,76 @@ them directly. Instead, use the CRM.Backbone.extendModel and CRM.Backbone.extendCollection helpers to mix-in the necessary properties. -#### CRM.Backbone.extend(Model,Collection) +### `CRM.Backbone.extend(Model,Collection)` To define models & collections which APIv3 for persistence, use the -extendModel() and extendCollection() helpers. - -<div class="code panel" style="border-width: 1px;"> - -<div class="codeHeader panelHeader" style="border-bottom-width: 1px;"> - -**Define model & collection classes** - -</div> - -<div class="codeContent panelContent"> - - var ContactModel = Backbone.Model.extend({ - ... - }); - CRM.Backbone.extendModel(ContactModel, "Contact"); - - var ContactCollection = Backbone.Collection.extend({ - model: ContactModel - }); - CRM.Backbone.extendCollection(ContactCollection); - -</div> - -</div> - -<div class="code panel" style="border-width: 1px;"> - -<div class="codeHeader panelHeader" style="border-bottom-width: 1px;"> - -**Create a new contact record** - -</div> - -<div class="codeContent panelContent"> - - var contact = new ContactModel({ - contact_type: 'Individual', - first_name: 'Bat', - last_name: 'Man' - }); - contact.save({}, { - success: function() { ... } - error: function() { ... } - }); - -</div> - -</div> - -<div class="code panel" style="border-width: 1px;"> - -<div class="codeHeader panelHeader" style="border-bottom-width: 1px;"> - -**Load a specific contact record** - -</div> - -<div class="codeContent panelContent"> - - var contact = new ContactModel({ - id: 123 - }); - contact.fetch({ - success: function() { ... } - error: function() { ... } - }); - -</div> - -</div> - -<div class="code panel" style="border-width: 1px;"> - -<div class="codeHeader panelHeader" style="border-bottom-width: 1px;"> - -**Load all organizations** - -</div> - -<div class="codeContent panelContent"> - - var contacts = new ContactCollection([], { - // crmCriteria defines query parameters per APIv3 - crmCriteria: {contact_type: 'Organization'} - }); - contacts.fetch({ - success: function() { - console.log("Loaded " + contacts.length + " contact(s)"); - }, - error: function() { - console.log("Failed to load contacts"); - } - }); - -</div> - -</div> - -<div class="code panel" style="border-width: 1px;"> - -<div class="codeHeader panelHeader" style="border-bottom-width: 1px;"> - -**Update list of emails for contact #123 (immediately)** - -</div> - -<div class="codeContent panelContent"> - +`extendModel()` and `extendCollection()` helpers. + +```javascript +var ContactModel = Backbone.Model.extend({ + ... +}); +CRM.Backbone.extendModel(ContactModel, "Contact"); + +var ContactCollection = Backbone.Collection.extend({ + model: ContactModel +}); +CRM.Backbone.extendCollection(ContactCollection); +``` + +Create a new contact record + +```javascript +var contact = new ContactModel({ + contact_type: 'Individual', + first_name: 'Bat', + last_name: 'Man' +}); +contact.save({}, { + success: function() { ... } + error: function() { ... } +}); +``` + +Load a specific contact record + +```javascript +var contact = new ContactModel({ + id: 123 +}); +contact.fetch({ + success: function() { ... } + error: function() { ... } +}); +``` + +Load all organizations + +```javascript +var contacts = new ContactCollection([], { + // crmCriteria defines query parameters per APIv3 + crmCriteria: {contact_type: 'Organization'} +}); +contacts.fetch({ + success: function() { + console.log("Loaded " + contacts.length + " contact(s)"); + }, + error: function() { + console.log("Failed to load contacts"); + } +}); +``` + +Update list of emails for contact #123 (immediately) + +```javascript // Update the list of email addresses for contact 123 var emails = new EmailCollection([], { crmCriteria: {contact_id: 123} }); emails.fetch(...); - ... + //... // Add an email on client and server (with an immediate AJAX request) var email = emails.create({ @@ -426,87 +235,72 @@ extendModel() and extendCollection() helpers. email: 'new-email@example.com' }, ...); - - ... + //... // Modify an email on client and server (with an immediate AJAX request) emails.get(456).set('on_hold', 1); emails.get(456).save(...); - ... + //... // Remove an email on client and server (with an immediate AJAX request) emails.get(789).destroy(...); - -</div> - -</div> - -<div class="code panel" style="border-width: 1px;"> - -<div class="codeHeader panelHeader" style="border-bottom-width: 1px;"> - -**Update list of emails for contact #123 (delayed-save)** - -</div> - -<div class="codeContent panelContent"> - - // Update the list of email addresses for contact 123 - var emails = new EmailCollection([], { - crmCriteria: {contact_id: 123} - }); - emails.fetch(...); - - ... - - // Add a new email on client (but don't send to server yet) - var email = new EmailModel({ - contact_id: 123, - email: 'another-email@example.com' - }); - emails.add(model); - - // Update an email on client (but don't send to server yet) - emails.get(456).set('on_hold', 1); - - // Remove an email on client (but don't send to server yet) - emails.get(789).setSoftDeleted(true); - - ... - - // Send all changes to all emails to server (with one AJAX call) - emails.save(...); - - // NOTE: Use this carefully. This will perform INSERTs, UPDATEs, and/or DELETEs - // to make the email list match on the client and server. The server will use - // an algorithm like this: - // - // 1. Accept list of records from client. - // 2. Query list of pre-existing emails on server (matching crmCriteria). - // 3. Identify records in BOTH client and server. UPDATE them. - // 4. Identify records in CLIENT but not server. INSERT them. - // 5. Identify records in SERVER but not client. DELETE them. - // - // This is generally appropriate when you know the client has the full, - // proper collection -- e.g. it's appropriate with the collection of "Email", - // "Phone", or "Address" records of one contact. However, it's not appropriate - // for a collection of "Activities" (because concurrent processes may add new - // activities that are unknown the client -- and those records shouldn't be - // deleted). If you have a use-case that needs a different / more nuanced - // reconciliation strategy, post to the forum to discuss. - -</div> - -</div> +``` + +Update list of emails for contact #123 (delayed-save) + +```javascript +// Update the list of email addresses for contact 123 +var emails = new EmailCollection([], { + crmCriteria: {contact_id: 123} +}); +emails.fetch(...); + +... + +// Add a new email on client (but don't send to server yet) +var email = new EmailModel({ + contact_id: 123, + email: 'another-email@example.com' +}); +emails.add(model); + +// Update an email on client (but don't send to server yet) +emails.get(456).set('on_hold', 1); + +// Remove an email on client (but don't send to server yet) +emails.get(789).setSoftDeleted(true); + +... + +// Send all changes to all emails to server (with one AJAX call) +emails.save(...); + +// NOTE: Use this carefully. This will perform INSERTs, UPDATEs, and/or DELETEs +// to make the email list match on the client and server. The server will use +// an algorithm like this: +// +// 1. Accept list of records from client. +// 2. Query list of pre-existing emails on server (matching crmCriteria). +// 3. Identify records in BOTH client and server. UPDATE them. +// 4. Identify records in CLIENT but not server. INSERT them. +// 5. Identify records in SERVER but not client. DELETE them. +// +// This is generally appropriate when you know the client has the full, +// proper collection -- e.g. it's appropriate with the collection of "Email", +// "Phone", or "Address" records of one contact. However, it's not appropriate +// for a collection of "Activities" (because concurrent processes may add new +// activities that are unknown the client -- and those records shouldn't be +// deleted). If you have a use-case that needs a different / more nuanced +// reconciliation strategy, post to the forum to discuss. +``` ## Unit Tests The CiviCRM Backbone plugins are tested with qUnit. To run the unit-tests, use a web-browser to connect to a CiviCRM installation -("http://local.example.com") and request -"*http://local.example.com*/**civicrm/tests/qunit/crm-backbone**" +(`http://local.example.com`) and request the following: + +`http://local.example.com/civicrm/tests/qunit/crm-backbone` -The source for the unit-tests are stored in "tests/qunit/crm-backbone" -(e.g. -<https://github.com/civicrm/civicrm-core/tree/master/tests/qunit/crm-backbone>). +The source for the unit-tests are stored in ["tests/qunit/crm-backbone"](https://github.com/civicrm/civicrm-core/tree/master/tests/qunit/crm-backbone).