Skip to content
Snippets Groups Projects
Commit 40d6cd5b authored by Sean Madsen's avatar Sean Madsen
Browse files

Clean up content

parent 83121d88
No related branches found
No related tags found
No related merge requests found
# 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).
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment