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
1 merge request!286Migrate "Backbone Reference" wiki page
# 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