Skip to content
This repository has been archived by the owner on Sep 14, 2019. It is now read-only.

Commit

Permalink
added support and tests for binding contexts (aka persistent/transien…
Browse files Browse the repository at this point in the history
…t models)
  • Loading branch information
dzrw committed Feb 13, 2012
1 parent a6ced5d commit b13664a
Show file tree
Hide file tree
Showing 9 changed files with 145 additions and 55 deletions.
30 changes: 26 additions & 4 deletions README.md
Expand Up @@ -6,9 +6,17 @@ Setup up your Backbone.View like so:

```CoffeeScript

class TodoModel extends Backbone.Model
defaults:
todo: 'Learn outback.js'

class TodoView extends Backbone.View
model: TodoModel

# setup a viewModel for transient state (optional)
viewModel: new Backbone.Model
isEditing: false

@render: ->
Backbone.outback.bind @

Expand All @@ -21,22 +29,36 @@ Sprinkle data-bind attributes into your templates:

```HTML

<div id="edit">
<input type="text" class="todo-input" data-bind="value: @todo">
<!-- adds the CSS class 'editing' to the div when the
viewModel's isEditing attribute is true, removes it
when it isn't -->

<div data-bind-view="css: { editing: @isEditing }">

<!-- the input's value is two-way bound to the model's todo
attribute, and the focus state of the control is two-way
bound to the viewModel's isEditing attribute -->

<input type="text"
class="todo-input"
data-bind="value: @todo"
data-bind-view="hasfocus: @isEditing">

</div>

```

## Can I configure the bindings in code instead?

Yes, outback bindings can be setup in either code or markup.
Yes, outback bindings can be setup in either code or markup; feel free to mix and match.

```CoffeeScript

class TodoView extends Backbone.View
model: TodoModel

dataBindings:
# modelBindings (viewModelBindings) are bound to your model (viewModel)
modelBindings:
'#edit .todo-input':
value: Backbone.outback.modelRef 'todo'

Expand Down
19 changes: 0 additions & 19 deletions TODO
Expand Up @@ -2,24 +2,5 @@

- add bindings (options, selectedOptions, uniqueName?)

- binding to the view instead of the model

option 1: view/model paradigm
data-bind="visible: @isSelected, visibleOptions: { durable: false }"

---or---

option 2: nested views paradigm

data-bind="visible: @isSelected, visibleOptions: { dataSource: '//' }"
data-bind="visible: @isSelected, visibleOptions: { dataSource: '//model' }" (default)

---or---

option 3: use a different attribute

data-bind-view="visible: @isSelected"


- dependency cycle detection / breakage

71 changes: 48 additions & 23 deletions outback.js
Expand Up @@ -97,6 +97,7 @@
modelAttrName: modelAttrName,
valueAccessor: makeValueAccessorBuilder(model),
modelEvents: {
eventName: false, // TODO: Defaults to "change:modelAttrName"
subscribe: subscribe,
unsubscribe: unsubscribe
}
Expand Down Expand Up @@ -135,33 +136,33 @@
};
}

function parseDataBindAttrBindingDecls (view, model) {
function parseDataBindAttrBindingDecls (databindAttr, view, model) {
var bindingDecls, selector;

bindingDecls = [];
selector = "*[data-bind]";
selector = "*["+databindAttr+"]";

view.$(selector).each(function () {
var element, bindingExpr, directives;

element = view.$(this);
bindingExpr = element.attr('data-bind');
bindingExpr = element.attr(databindAttr);
directives = rj.parse(bindingExpr, makeBindingDeclReviver(model));

bindingDecls.push({
element: element,
directives: directives
directives: directives,
dataSource: model
});
});

return bindingDecls;
}

function parseUnobtrusiveBindingDecls (view, model) {
var bindingDecls, root, viewAttr;
function parseUnobtrusiveBindingDecls (viewAttr, view, model) {
var bindingDecls, root;

bindingDecls = [];
viewAttr = 'dataBindings'; // TODO: hard-coded view attribute should be configurable

_.each(view[viewAttr], function(value, selector) {
if(!hop(view[viewAttr], selector)) return;
Expand All @@ -174,7 +175,8 @@

bindingDecls.push({
element: element,
directives: directives
directives: directives,
dataSource: model
});
}
});
Expand Down Expand Up @@ -254,7 +256,7 @@
}

function applyBinding (view, binding) {
var binders, binderArgs, modelEventName, updateFn;
var binders, binderArgs, eventName, updateFn;

binders = {
modelSubs: [],
Expand All @@ -264,25 +266,27 @@
modelUnsubs: []
};

modelEventName = typeof binding.modelEventName === 'string' ? binding.modelEventName : false;
eventName = binding.modelEvents.eventName;
binderArgs = [binding.element, binding.valueAccessor, binding.allBindingsAccessor, view];

function nop() {}
if (!hop(binding.handler, 'update')) {
return undefined;
}

updateFn = hop(binding.handler, 'update') ? binding.handler.update : nop;
updateFn = binding.handler.update;

binders.updates.push(function() {
updateFn.apply(view, binderArgs);
});

binders.modelSubs.push(function() {
binding.modelEvents.subscribe(modelEventName, function(m, val) {
binding.modelEvents.subscribe(eventName, function(m, val) {
updateFn.apply(view, binderArgs);
});
});

binders.modelUnsubs.push(function () {
binding.modelEvents.unsubscribe(modelEventName);
binding.modelEvents.unsubscribe(eventName);
})

if (hop(binding.handler, 'init')) {
Expand All @@ -304,8 +308,8 @@
this.modelAttrName = modelAttrName;
};

var OutbackBinder = function (view, model, bindingHandlers) {
var allBinders;
var OutbackBinder = function (view, bindingHandlers) {
var bindingContexts, allBinders;

allBinders = {
modelSubs: [],
Expand All @@ -314,17 +318,38 @@
removes: [],
modelUnsubs: []
};

bindingContexts = {
model: {
dataSource: view.model,
databindAttr: 'data-bind',
unobtrusiveAttr: 'modelBindings'
},
viewModel: {
dataSource: view.viewModel,
databindAttr: 'data-bind-view',
unobtrusiveAttr: 'viewModelBindings'
}
};

function arrayConcatBindingContext(bindingDecls, context) {
if(context.dataSource) {
arrayConcat(bindingDecls, parseDataBindAttrBindingDecls(context.databindAttr, view, context.dataSource));
arrayConcat(bindingDecls, parseUnobtrusiveBindingDecls(context.unobtrusiveAttr, view, context.dataSource));
}
}

this.bind = function () {
var bindingDecls, bindings, summary;

summary = {
executableBindingsSkipped: 0,
executableBindingsInstalled: 0
};

bindingDecls = [];
arrayConcat(bindingDecls, parseDataBindAttrBindingDecls(view, model));
arrayConcat(bindingDecls, parseUnobtrusiveBindingDecls(view, model));
arrayConcatBindingContext(bindingDecls, bindingContexts.model);
arrayConcatBindingContext(bindingDecls, bindingContexts.viewModel);

bindings = [];
_.each(bindingDecls, function (bindingDecl) {
Expand All @@ -339,6 +364,10 @@

_.each(bindings, function (binding) {
binders = applyBinding(view, binding);
if (_.isUndefined(binders)) {
summary.executableBindingsSkipped++;
return;
}

arrayConcat(allBinders.modelSubs, binders.modelSubs);
arrayConcat(allBinders.inits, binders.inits);
Expand All @@ -355,10 +384,6 @@
eachfn(allBinders.updates);
eachfn(allBinders.modelSubs);

// Fire a single model change event to sync the DOM.

//model.trigger('change');

if (typeof view.bindingSummary === 'function') {
view.bindingSummary(summary);
}
Expand All @@ -376,7 +401,7 @@
Backbone.outback = {
version: "0.1.0",
bind: function(view){
view.__outback_binder = new OutbackBinder(view, view.model, Backbone.outback.bindingHandlers); // TODO: Support for Collections
view.__outback_binder = new OutbackBinder(view, Backbone.outback.bindingHandlers);
view.__outback_binder.bind();
},

Expand Down
2 changes: 1 addition & 1 deletion spec/SpecRunner.html
Expand Up @@ -60,7 +60,7 @@
<script type="text/javascript" src="bindings/value.spec.js"></script>
<script type="text/javascript" src="bindings/hasfocus.spec.js"></script>
<script type="text/javascript" src="bindings/checked.spec.js"></script>

<!---->
</head>
<body>
<div id="jasmine_content"></div>
Expand Down
4 changes: 2 additions & 2 deletions spec/bindings/text.spec.js
Expand Up @@ -33,7 +33,7 @@ describe('the text binding', function() {
this.view = new FixtureView({model: this.model});
_.extend(this.view, {
innerHtml: "<p></p>",
dataBindings: {
modelBindings: {
'#anchor p': {
text: Backbone.outback.modelRef('content')
}
Expand All @@ -59,7 +59,7 @@ describe('the text binding', function() {
});

it('should allow you to shoot yourself in the foot', function () {
this.view.dataBindings['#anchor p'].textOptions = { escape: false };
this.view.modelBindings['#anchor p'].textOptions = { escape: false };
expect(this.view).toHaveAnElementWithContent('#anchor p', xssPayload);
});

Expand Down
4 changes: 2 additions & 2 deletions spec/bindings/value.spec.js
Expand Up @@ -43,7 +43,7 @@ describe('the value binding', function() {
this.view = new FixtureView({model: this.model});
_.extend(this.view, {
innerHtml: "<input type='text'>",
dataBindings: {
modelBindings: {
'#anchor input': {
value: Backbone.outback.modelRef('content')
}
Expand All @@ -69,7 +69,7 @@ describe('the value binding', function() {
});

it('should allow you to shoot yourself in the foot', function () {
this.view.dataBindings['#anchor input'].valueOptions = { escape: false };
this.view.modelBindings['#anchor input'].valueOptions = { escape: false };
expect(this.view).toHaveAnElementWithContent('#anchor input', xssPayload);
});

Expand Down
4 changes: 2 additions & 2 deletions spec/bindings/visible.spec.js
Expand Up @@ -5,7 +5,7 @@ describe('the visible binding', function () {
this.model = new AModel({isVisible: true});
this.view = new FixtureView({model: this.model});
_.extend(this.view, {
dataBindings: {
modelBindings: {
'#anchor': {
visible: Backbone.outback.modelRef('isVisible')
}
Expand All @@ -31,7 +31,7 @@ describe('the visible binding', function () {
this.model = new AModel({isVisible: true});
this.view = new FixtureView({model: this.model});
_.extend(this.view, {
dataBindings: {
modelBindings: {
'#anchor': {
visible: Backbone.outback.modelRef('isVisible'),
visibleOptions: { not: true }
Expand Down

0 comments on commit b13664a

Please sign in to comment.