UNPKG

backbone.epoxy

Version:

Elegant data binding for Backbone.js

1,233 lines (945 loc) 37.2 kB
// Epoxy.Model // ----------- describe("Backbone.Epoxy.Model", function() { var model; // Primay model for test suite: var TestModel = Backbone.Epoxy.Model.extend({ defaults: { firstName: "Charlie", lastName: "Brown", payment: 100, isSelected: false, testArray: [] }, computeds: { // Tests setting a computed property with the direct single-function getter shorthand: fullName: function() { return this.get( "firstName" ) +" "+ this.get( "lastName" ); }, // Tests two facets: // 1) computed dependencies definition order (defined before/after a dependency). // 2) computed dependencies building ontop of one another. paymentLabel: function() { return this.get( "fullName" ) +" paid "+ this.get( "paymentCurrency" ); }, // Tests defining a read/write computed property with getters and setters: paymentCurrency: { get: function() { return "$"+ this.get( "payment" ); }, set: function( value ) { return value ? {payment: parseInt(value.replace("$", ""), 10)} : value; } }, // Tests defining a computed property with unreachable values... // first/last names are accessed conditionally, therefore cannot be automatically detected. // field dependencies may be declared manually to address this (ugly though); // a better solution would be to collect both "first" and "last" as local vars, // then release the locally established values conditionally. unreachable: { deps: ["firstName", "lastName", "isSelected"], get: function() { return this.get("isSelected") ? this.get("lastName") : this.get("firstName"); } } }, initialize: function() { } }); // Secondary model, established for some relationship testing: var ForeignModel = Backbone.Epoxy.Model.extend({ defaults: { avgPayment: 200 } }); // Setup beforeEach(function() { model = new TestModel(); }); // Teardown afterEach(function() { model.clearComputeds(); model = null; }); it("should construct model with class options defined.", function() { var obj = {}; var model = new Backbone.Epoxy.Model({}, { computeds: obj }); expect( model.computeds ).to.equal( obj ); }); it("should construct model with class options defined.", function() { var obj = {}; var model = new Backbone.Epoxy.Model({}, { computeds: obj }); expect( model.computeds ).to.equal( obj ); }); it("should allow Epoxy model configuration to mixin with another Backbone Model.", function() { var MixinModel = Backbone.Model.extend({ defaults: { avgPayment: 500 }, initialize: function() { this.initComputeds(); }, computeds: { avgPaymentDsp: function() { return "$"+this.get( "avgPayment" ); } } }); Backbone.Epoxy.Model.mixin( MixinModel.prototype ); var model = new MixinModel(); expect( model.get("avgPaymentDsp") ).to.equal( "$500" ); }); it("should use .get() and .set() to modify native properties.", function() { model.set( "isSelected", true ); expect( model.get("isSelected") ).to.equal( true ); }); it("should get native model attributes using '.toJSON()'.", function() { var json = model.toJSON(); expect( _.size(json) ).to.equal( 5 ); }); it("should get native and computed model attributes using '.toJSON({computed:true})'.", function() { var json = model.toJSON({computed:true}); expect( _.size(json) ).to.equal( 9 ); expect( json.fullName ).to.equal( "Charlie Brown" ); }); it("should allow direct management of array attributes using the '.modifyArray' method.", function() { expect(model.get( "testArray" )).to.have.length( 0 ); model.modifyArray("testArray", "push", "beachball"); expect(model.get( "testArray" )).to.have.length( 1 ); }); it("should defer all action when using '.modifyArray' on a non-array object.", function() { model.modifyArray("isSelected", "push", "beachball"); expect( model.get( "isSelected" ) ).to.equal( false ); }); it("should assume computed properties defined as functions to be getters.", function() { var obsGetter = model._c.fullName._get; var protoGetter = TestModel.prototype.computeds.fullName; expect( obsGetter === protoGetter ).to.equal( true ); }); it("should use '.computeds' to automatically construct computed properties.", function() { var hasFullName = model.hasComputed("fullName"); var hasDonation = model.hasComputed("paymentCurrency"); expect( hasFullName && hasDonation ).to.equal( true ); }); it("should allow computed properties to be constructed out of dependency order (dependents may preceed their dependencies).", function() { expect( model.get("paymentLabel") ).to.equal( "Charlie Brown paid $100" ); }); it("should allow computed properties to be defined with manual dependency declarations.", function() { // Test initial reachable value: expect( model.get("unreachable") ).to.equal( "Charlie" ); // Change conditional value to point at the originally unreachable value: model.set("isSelected", true); expect( model.get("unreachable") ).to.equal( "Brown" ); // Change unreachable value model.set("lastName", "Black"); expect( model.get("unreachable") ).to.equal( "Black" ); }); it("should inject manual dependency declarations as getter arguments.", function() { model.addComputed("getterInjection", function(first, last) { return first +" "+ last; }, "firstName", "lastName"); expect( model.get("getterInjection") ).to.equal( "Charlie Brown" ); }); it("should use .addComputed() to define computed properties.", function() { model.addComputed("nameReverse", function() { return this.get("lastName") +", "+ this.get("firstName"); }); expect( model.get("nameReverse") ).to.equal( "Brown, Charlie" ); }); it("should use .addComputed() to define properties with passed dependencies.", function() { model.addComputed("unreachable", function() { return this.get("payment") > 50 ? this.get("firstName") : this.get("lastName"); }, "payment", "firstName", "lastName"); // Test initial reachable value: expect( model.get("unreachable") ).to.equal( "Charlie" ); // Change conditional value to point at the originally unreachable value: model.set("payment", 0); expect( model.get("unreachable") ).to.equal( "Brown" ); // Change unreachable value model.set("lastName", "Black"); expect( model.get("unreachable") ).to.equal( "Black" ); }); it("should use .addComputed() to define new properties from a params object.", function() { model.addComputed("addedProp", { deps: ["payment", "firstName", "lastName"], get: function() { return this.get("payment") > 50 ? this.get("firstName") : this.get("lastName"); }, set: function( value ) { return {payment: value}; } }); // Test initial reachable value: expect( model.get("addedProp") ).to.equal( "Charlie" ); // Change conditional value to point at the originally unreachable value: model.set("payment", 0); expect( model.get("addedProp") ).to.equal( "Brown" ); // Change unreachable value model.set("lastName", "Black"); expect( model.get("addedProp") ).to.equal( "Black" ); // Set computed value model.set("addedProp", 123); expect( model.get("payment") ).to.equal( 123 ); }); it("should use .get() to access both model attributes and computed properties.", function() { var firstName = (model.get("firstName") === "Charlie"); var fullName = (model.get("fullName") === "Charlie Brown"); expect( firstName && fullName ).to.equal( true ); }); it("should automatically map and bind computed property dependencies.", function() { var fullPre = (model.get( "fullName" ) === "Charlie Brown"); model.set( "lastName", "Black" ); var fullPost = (model.get( "fullName" ) === "Charlie Black"); expect( fullPre && fullPost ).to.equal( true ); }); it("should automatically map and bind computed property dependencies on foreign Epoxy models.", function() { var averages = new ForeignModel(); model.addComputed("percentAvgPayment", function() { return this.get("payment") / averages.get("avgPayment"); }); expect( model.get("percentAvgPayment") ).to.equal( 0.5 ); averages.set("avgPayment", 400); expect( model.get("percentAvgPayment") ).to.equal( 0.25 ); averages.clearComputeds(); }); it("should manage extended graphs of computed dependencies.", function() { expect( model.get("paymentLabel") ).to.equal( "Charlie Brown paid $100" ); model.set("payment", 150); expect( model.get("paymentLabel") ).to.equal( "Charlie Brown paid $150" ); }); it("should use .set() to modify normal model attributes.", function() { model.set("payment", 150); expect( model.get("payment") ).to.equal( 150 ); expect( model.get("paymentCurrency") ).to.equal( "$150" ); }); it("should use .set() for virtual computed properties to pass values along to the model.", function() { expect( model.get("payment") ).to.equal( 100 ); model.set("paymentCurrency", "$200"); expect( model.get("payment") ).to.equal( 200 ); expect( model.get("paymentCurrency") ).to.equal( "$200" ); }); it("should throw .set() error when modifying read-only computed properties.", function() { function testForError() { model.set("fullName", "Charlie Black"); } expect( testForError ).to.throw(); }); it("should use .set() to allow computed properties to cross-set one another.", function() { model.addComputed("crossSetter", { get: function() { return this.get("isSelected"); }, set: function( value ) { return {isSelected: value}; } }); expect( model.get("crossSetter") ).to.equal( false ); model.set("crossSetter", true ); expect( model.get("isSelected") ).to.equal( true ); }); it("should throw .set() error in response to circular setter references.", function() { model.addComputed("loopSetter1", { get: function() { return "Nothing"; }, set: function( value ) { return {loopSetter2: false}; } }); model.addComputed("loopSetter2", { get: function() { return "Nothing"; }, set: function( value ) { return {loopSetter1: false}; } }); function circularRef() { model.set("loopSetter1", true ); } expect( circularRef ).to.throw(); }); }); // Epoxy.View // ---------- describe("Backbone.Epoxy.View", function() { // Collection test components: var CollectionView = Backbone.Epoxy.View.extend({ el: "<li><span class='name-dsp' data-bind='text:name'></span> <button class='name-remove'>x</button></li>" }); var TestCollection = Backbone.Collection.extend({ model: Backbone.Model }); // Test model: window.dataModel = new (Backbone.Epoxy.Model.extend({ defaults: { firstName: "Luke", lastName: "Skywalker", preference: "b", active: true, valOptions: "1", valDefault: "1", valEmpty: "1", valBoth: "1", valMulti: "1", valCollect: "" }, computeds: { firstNameError: function() { return !this.get( "firstName" ); }, lastNameError: function() { return !this.get( "lastName" ); }, errorDisplay: function() { var first = this.get( "firstName" ); var last = this.get( "lastName" ); return (!first || !last) ? "block" : "none"; } } })); window.viewModel = new (Backbone.Epoxy.Model.extend({ defaults: { checkList: ["b"], optionsList: [ {value: "0", label: "Luke Skywalker"}, {value: "1", label: "Han Solo"}, {value: "2", label: "Obi-Wan Kenobi"} ], optDefault: "default", optEmpty: "empty" } })); // Basic bindings test view: var domView = new (Backbone.Epoxy.View.extend({ el: "#dom-view", model: dataModel, viewModel: viewModel, bindings: "data-bind", bindingHandlers: { printArray: function( $element, value ) { $element.text( value.slice().sort().join(", ") ); }, sayYesNo: { get: function( $element ) { return {active: $element.val().indexOf("Y") === 0 }; }, set: function( $element, value ) { $element.val( value ? "Y" : "N" ); } } }, computeds: { checkedCount: function() { return "Checked items: "+ this.getBinding("checkList").length; }, nameDisplay: { deps: ["lastName", "firstName"], get: function( lastName, firstName ) { return "<strong>"+ lastName +"</strong>, "+ firstName; }, set: function( value ) { this.nameDisplaySetterValue = value; } } } })); // Modifiers / Collections testing view: var modView = new (Backbone.Epoxy.View.extend({ el: "#mod-view", model: dataModel, viewModel: viewModel, collection: new TestCollection(), itemView: CollectionView, bindings: "data-bind", events: { "click .name-add": "onAddName", "click .name-remove": "onRemoveName" }, onAddName: function() { var input = this.$( ".name-input" ); if ( input.val() ) { this.collection.add({ name: input.val() }); input.val(""); } }, onRemoveName: function( evt ) { var i = $( evt.target ).closest( "li" ).index(); this.collection.remove( this.collection.at(i) ); } })); // Bindings map declaration: var tmplView = new (Backbone.Epoxy.View.extend({ el: $("#tmpl-view-tmpl").html(), model: dataModel, viewModel: viewModel, bindings: { ".user-first": "text:firstName", ".user-last": "text:lastName" }, initialize: function() { $("#tmpl-view-tmpl").after( this.$el ); } })); // Setup beforeEach(function() { }); // Teardown afterEach(function() { var defaults = _.clone( viewModel.defaults ); defaults.checkList = _.clone( defaults.checkList ); defaults.optionsList = _.clone( defaults.optionsList ); dataModel.set( dataModel.defaults ); viewModel.set( defaults ); modView.collection.reset(); }); // Simple visibility check to replace ".is(':visible')", for basic Zepto caompatibility: function isVisible($el) { var dsp = $el.css("display"); return dsp !== 'none'; } it("should construct view with class options defined.", function() { var model = new Backbone.Model(); var obj = {}; var view = new Backbone.Epoxy.View({ model: model, viewModel: model, computeds: obj, bindings: "my-binding", bindingFilters: obj, bindingHandlers: obj, bindingSources: obj }); expect( view.model ).to.equal( model ); expect( view.viewModel ).to.equal( model ); expect( view.computeds ).to.equal( obj ); expect( view.bindings ).to.equal( "my-binding" ); expect( view.bindingFilters ).to.equal( obj ); expect( view.bindingHandlers ).to.equal( obj ); expect( view.bindingSources ).to.exist; // << "obj" is copied, so has new identity. }); it("should allow Epoxy view configuration to mixin with another Backbone View.", function() { var MixinView = Backbone.View.extend({ el: "<div data-bind='text:ship'></div>", model: new Backbone.Model({ship:"Deathstar"}), initialize: function() { this.applyBindings(); } }); Backbone.Epoxy.View.mixin( MixinView.prototype ); var view = new MixinView(); expect( view.$el.text() ).to.equal( "Deathstar" ); }); it("should allow Epoxy view configuration to mixin with another Backbone View but not overriding bindings.", function() { var MixinView = Backbone.View.extend({ el: "<div data-epoxy='text:ship'></div>", model: new Backbone.Model({ship:"Deathstar"}), bindings: 'data-epoxy', initialize: function() { this.applyBindings(); } }); Backbone.Epoxy.View.mixin( MixinView.prototype ); var view = new MixinView(); expect( view.$el.text() ).to.equal( "Deathstar" ); }); it("should bind view elements to model via binding selector map.", function() { var $el = $("#tmpl-view .user-first"); expect( $el.text() ).to.equal( "Luke" ); }); it("should bind view elements to model via element attribute query.", function() { var $el = $("#dom-view .test-text-first"); expect( $el.text() ).to.equal( "Luke" ); }); it("should bind view element to model via binding selector map in object notation.", function() { var view = new (Backbone.Epoxy.View.extend({ el: "<span class='first-name'></span>", model: dataModel, bindings: { ".first-name": { text: 'firstName', attr: { title: 'format("$1 $2", firstName, lastName)', 'data-last-name': 'lastName', '"data-first-name"': 'firstName' } } } })); expect( view.$el.text() ).to.equal( "Luke" ); expect( view.$el.attr('title') ).to.equal( "Luke Skywalker" ); expect( view.$el.attr('data-last-name') ).to.equal( "Skywalker" ); expect( view.$el.attr('data-first-name') ).to.equal( "Luke" ); }); it("should include top-level view container in bindings searches.", function() { var view1 = new (Backbone.Epoxy.View.extend({ el: "<span data-bind='text:firstName'></span>", model: dataModel, bindings: "data-bind" })); var view2 = new (Backbone.Epoxy.View.extend({ el: "<span class='first-name'></span>", model: dataModel, bindings: { ".first-name": "text:firstName" } })); expect( view1.$el.text() ).to.equal( "Luke" ); expect( view2.$el.text() ).to.equal( "Luke" ); }); it("should include top-level view container in bindings searches via :el selector.", function() { var view = new (Backbone.Epoxy.View.extend({ el: "<span></span>", model: dataModel, bindings: { ":el": "text:firstName" } })); expect( view.$el.text() ).to.equal( "Luke" ); }); it("should include top-level view container in bindings searches via :scope selector.", function() { var view = new (Backbone.Epoxy.View.extend({ el: "<span></span>", model: dataModel, bindings: { ":scope": "text:firstName" } })); expect( view.$el.text() ).to.equal( "Luke" ); }); it("should throw error in response to undefined property bindings.", function() { var ErrorView = Backbone.Epoxy.View.extend({ el: "<div><span data-bind='text:undefinedProp'></span></div>", model: dataModel, bindings: "data-bind" }); function testForError(){ var error = new ErrorView(); } expect( testForError ).to.throw(); }); it("should allow custom bindings to set data into the view.", function() { var $els = $(".test-custom-binding"); expect( $els.text() ).to.equal( "b" ); viewModel.set("checkList", ["c","a"]); expect( $els.text() ).to.equal( "a, c" ); }); it("should allow custom bindings to get data from the view.", function() { var $el = $(".test-yes-no"); expect( $el.val() ).to.equal( "Y" ); // Change through model, look for view change: dataModel.set("active", false); expect( $el.val() ).to.equal( "N" ); // Change through view, look for model change: $el.val( "Y" ).trigger( "change" ); expect( dataModel.get("active") ).to.equal( true ); }); it("should use '.getBinding()' to read data from binding sources.", function() { expect( modView.getBinding("firstName") ).to.equal( "Luke" ); }); it("should use '.setBinding()' to write data into binding sources.", function() { var $el = $(".test-text-first"); modView.setBinding( "firstName", "Leia" ); expect( dataModel.get("firstName") ).to.equal( "Leia" ); expect( $el.text() ).to.equal( "Leia" ); }); it("should generate computed view properties based on '.computeds' hash.", function() { expect( domView.getBinding("checkedCount") ).to.equal( "Checked items: 1" ); domView.viewModel.modifyArray("checkList", "push", "c"); expect( domView.getBinding("checkedCount") ).to.equal( "Checked items: 2" ); }); it("should allow view bindings to map through computed properties.", function() { var $el = $(".test-view-computed"); expect( $el.text() ).to.equal( "Checked items: 1" ); domView.viewModel.modifyArray("checkList", "push", "c"); expect( $el.text() ).to.equal( "Checked items: 2" ); }); it("should inject manual dependency declarations as computed getter arguments.", function() { expect( domView.getBinding("nameDisplay") ).to.equal( "<strong>Skywalker</strong>, Luke" ); }); it("should allow computed view properties to recieve and store data.", function() { expect( domView.nameDisplaySetterValue ).to.be.undefined; domView.setBinding("nameDisplay", "hello"); expect( domView.nameDisplaySetterValue ).to.equal( "hello" ); }); it("should allow multiple data sources and their namespaced attributes to be defined through 'bindingSources'.", function() { var m1 = new Backbone.Model({name: "Luke"}); var m2 = new Backbone.Collection(); var m3 = new Backbone.Model({name: "Han"}); var m4 = new Backbone.Collection(); var v1, v2, v3, v4, v5, v6; var sourceView = new (Backbone.Epoxy.View.extend({ el: "<div data-bind='b1:$model, b2:$collection, b3:$mod2, b4:$col2, b5:name, b6:mod2_name'></div>", model: m1, collection: m2, bindingSources: { mod2: m3, col2: m4 }, bindingHandlers: { b1: function( $el, value ) { v1 = value; }, b2: function( $el, value ) { v2 = value; }, b3: function( $el, value ) { v3 = value; }, b4: function( $el, value ) { v4 = value; }, b5: function( $el, value ) { v5 = value; }, b6: function( $el, value ) { v6 = value; } } })); expect( v1 ).to.equal( m1 ); expect( v2 ).to.equal( m2 ); expect( v3 ).to.equal( m3 ); expect( v4 ).to.equal( m4 ); expect( v5 ).to.equal( "Luke" ); expect( v6 ).to.equal( "Han" ); }); it("binding 'attr:' should establish a one-way binding with an element's attribute definitions.", function() { var $el = $(".test-attr-multi"); expect( $el.attr("href") ).to.equal( "b" ); expect( $el.attr("title") ).to.equal( "b" ); dataModel.set("preference", "c"); expect( $el.attr("href") ).to.equal( "c" ); expect( $el.attr("title") ).to.equal( "c" ); }); it("binding 'attr:' should allow string property definitions.", function() { var $el = $(".test-attr"); expect( $el.attr("data-active") ).to.equal( "true" ); dataModel.set("active", false); expect( $el.attr("data-active") ).to.equal( "false" ); }); it("binding 'checked:' should establish a two-way binding with a radio group.", function() { var $a = $(".preference[value='a']"); var $b = $(".preference[value='b']"); expect( $a.prop("checked") ).to.equal( false ); expect( $b.prop("checked") ).to.equal( true ); $a.prop("checked", true).trigger("change"); expect( dataModel.get("preference") ).to.equal( "a" ); }); it("binding 'checked:' should establish a two-way binding between a checkbox and boolean value.", function() { var $el = $(".test-checked-boolean"); expect( $el.prop("checked") ).to.equal( true ); $el.prop("checked", false).trigger("change"); expect( dataModel.get("active") ).to.equal( false ); }); it("binding 'checked:' should set a checkbox series based on a model array.", function() { var $els = $(".check-list"); // Default: populate based on intial setting: expect( !!$els.filter("[value='b']" ).prop("checked") ).to.equal( true ); expect( !!$els.filter("[value='c']" ).prop("checked") ).to.equal( false ); // Add new selection to the checkbox group: viewModel.set("checkList", ["b", "c"]); expect( !!$els.filter("[value='b']" ).prop("checked") ).to.equal( true ); expect( !!$els.filter("[value='c']" ).prop("checked") ).to.equal( true ); }); it("binding 'checked:' should respond to model changes performed by '.modifyArray'.", function() { var $els = $(".check-list"); // Add new selection to the checkbox group: expect( !!$els.filter("[value='b']" ).prop("checked") ).to.equal( true ); expect( !!$els.filter("[value='c']" ).prop("checked") ).to.equal( false ); viewModel.modifyArray("checkList", "push", "c"); expect( !!$els.filter("[value='b']" ).prop("checked") ).to.equal( true ); expect( !!$els.filter("[value='c']" ).prop("checked") ).to.equal( true ); }); it("binding 'checked:' should get a checkbox series formatted as a model array.", function() { var $els = $(".check-list"); dataModel.set("checkList", ["b"]); // Default: populate based on intial setting: expect( !!$els.filter("[value='b']" ).prop("checked") ).to.equal( true ); $els.filter("[value='a']").prop("checked", true).trigger("change"); expect( viewModel.get("checkList").join(",") ).to.equal( "b,a" ); }); it("binding 'classes:' should establish a one-way binding with an element's class definitions.", function() { var $el = $(".test-classes").eq(0); expect( $el.hasClass("error") ).to.equal( false ); expect( $el.hasClass("active") ).to.equal( true ); dataModel.set({ firstName: "", active: false }); expect( $el.hasClass("error") ).to.equal( true ); expect( $el.hasClass("active") ).to.equal( false ); }); it("binding 'collection:' should update display in response Backbone.Collection 'reset' events.", function() { var $el = $(".test-collection"); modView.collection.reset([ {name: "Luke Skywalker"} ]); expect( $el.children().length ).to.equal( 1 ); modView.collection.reset([ {name: "Hans Solo"}, {name: "Chewy"} ]); expect( $el.children().length ).to.equal( 2 ); }); it("binding 'collection:' should update display in response Backbone.Collection 'add' events.", function() { var $el = $(".test-collection"); modView.collection.add({name: "Luke Skywalker"}); expect( $el.children().length ).to.equal( 1 ); modView.collection.add([ {name: "Hans Solo"}, {name: "Chewy"} ]); expect( $el.children().length ).to.equal( 3 ); }); it("binding 'collection:' should update display in response Backbone.Collection 'remove' events.", function() { var $el = $(".test-collection"); modView.collection.add({name: "Luke Skywalker"}); expect( $el.children().length ).to.equal( 1 ); modView.collection.remove( modView.collection.at(0) ); expect( $el.children().length ).to.equal( 0 ); }); it("binding 'collection:' should update display in response Backbone.Collection 'sort' events.", function() { var $el = $(".test-collection"); modView.collection.reset([ {name: "B"}, {name: "A"} ]); expect( $el.find(":first-child .name-dsp").text() ).to.equal( "B" ); modView.collection.comparator = function( model ) { return model.get("name"); }; modView.collection.sort(); modView.collection.comparator = null; expect( $el.find(":first-child .name-dsp").text() ).to.equal( "A" ); }); it("binding 'collection:' child views should update display based on their model bindings.", function() { var modViewWithInitialCollection = new (Backbone.Epoxy.View.extend({ collection: new TestCollection([{name: "A"}]), itemView: CollectionView, el: "<div data-bind='collection:$collection'></div>", })); expect( modViewWithInitialCollection.$el.find(":first-child .name-dsp").text() ).to.equal( "A" ); modViewWithInitialCollection.collection.first().set({name: "B"}) expect( modViewWithInitialCollection.$el.find(":first-child .name-dsp").text() ).to.equal( "B" ); }); it("binding 'collection:' with 'itemView:' should define the collection view item.", function() { var modViewWithItemView = new (Backbone.Epoxy.View.extend({ collection: new TestCollection([{name: "A"}]), testView: Backbone.View.extend({el: '<span>test</span>'}), el: "<div data-bind=\"collection:$collection,itemView:'testView'\"></div>", })); expect( modViewWithItemView.$el.find(":first-child").text() ).to.equal( "test" ); }); it("binding 'css:' should establish a one-way binding with an element's css styles.", function() { var $el = $(".test-css"); expect( $el.css("display") ).to.equal( "none" ); dataModel.set( "lastName", "" ); expect( $el.css("display") ).to.equal( "block" ); }); it("binding 'disabled:' should establish a one-way binding with an element's disabled state.", function() { var $el = $(".test-disabled"); expect( $el.prop("disabled") ).to.be.true; dataModel.set( "active", false ); expect( $el.prop("disabled") ).to.be.false; }); it("binding 'enabled:' should establish a one-way binding with an element's inverted disabled state.", function() { var $el = $(".test-enabled"); expect( $el.prop("disabled") ).to.be.false; dataModel.set( "active", false ); expect( $el.prop("disabled") ).to.be.true; }); it("binding 'events:' should configure additional DOM event triggers.", function() { var $el = $(".test-input-first"); expect( $el.val() ).to.equal( "Luke" ); $el.val( "Anakin" ).trigger("keyup"); expect( dataModel.get("firstName") ).to.equal( "Anakin" ); }); it("binding 'html:' should establish a one-way binding with an element's html contents.", function() { var $el = $(".test-html"); // Compare markup as case insensitive to accomodate variances in browser DOM styling: expect( $el.html() ).to.match( /<strong>Skywalker<\/strong>, Luke/i ); dataModel.set("firstName", "Anakin"); expect( $el.html() ).to.match( /<strong>Skywalker<\/strong>, Anakin/i ); }); it("binding 'options:' should bind an array of strings to a select element's options.", function() { var $el = $(".test-select"); viewModel.set("optionsList", ["Luke", "Leia"]); expect( $el.children().length ).to.equal( 2 ); expect( $el.find(":first-child").attr("value") ).to.equal( "Luke" ); expect( $el.find(":first-child").text() ).to.equal( "Luke" ); }); it("binding 'options:' should bind an array of label/value pairs to a select element's options.", function() { var $el = $(".test-select"); viewModel.set("optionsList", [ {label:"Luke", value:"a"}, {label:"Leia", value:"b"} ]); expect( $el.children().length ).to.equal( 2 ); expect( $el.find(":first-child").attr("value") ).to.equal( "a" ); expect( $el.find(":first-child").text() ).to.equal( "Luke" ); }); it ("binding 'options:' should bind a collection of model label/value attributes to a select element's options.", function() { console.log('Test') var $el = $(".test-select-collect"); modView.collection.reset([ {label:"Luke Skywalker", value:"Luke"}, {label:"Han Solo", value:"Han"} ]); expect( $el.children().length ).to.equal( 2 ); expect( dataModel.get("valCollect") ).to.equal( "Luke" ); }); it("binding 'options:' should update selection when additional items are added/removed.", function() { var $el = $(".test-select"); viewModel.modifyArray("optionsList", "push", {label:"Leia", value:"3"}); expect( $el.children().length ).to.equal( 4 ); expect( $el.find(":last-child").attr("value") ).to.equal( "3" ); expect( $el.find(":last-child").text() ).to.equal( "Leia" ); }); it("binding 'options:' should preserve previous selection state after binding.", function() { var $el = $(".test-select"); viewModel.modifyArray("optionsList", "push", {label:"Leia", value:"3"}); expect( $el.children().length ).to.equal( 4 ); expect( $el.val() ).to.equal( "1" ); }); it("binding 'options:' should update the bound model value when the previous selection is no longer available.", function() { var $el = $(".test-select-default"); expect( dataModel.get("valDefault") ).to.equal( "1" ); viewModel.set("optionsList", []); expect( dataModel.get("valDefault") ).to.equal( "default" ); }); it("binding 'options:' should update a bound multiselect value when the previous selection is no longer available.", function() { var $el = $(".test-select-multi"); // Set two options as selected, and confirm they appear within the view: dataModel.set("valMulti", ["1", "2"]); expect( $el.val().join(",") ).to.equal( "1,2" ); // Remove one option from the list, then confirm the model captures the revised selection: viewModel.modifyArray("optionsList", "splice", 1, 1); expect( dataModel.get("valMulti").join(",") ).to.equal( "2" ); }); it("binding 'optionsDefault:' should include a default first option in a select menu.", function() { var $el = $(".test-select-default"); expect( $el.children().length ).to.equal( 4 ); expect( $el.find(":first-child").text() ).to.equal( "default" ); }); it("binding 'optionsDefault:' should bind the default option value to a model.", function() { var $el = $(".test-select-default"); viewModel.set("optDefault", {label:"choose...", value:""}); expect( $el.find(":first-child").text() ).to.equal( "choose..." ); }); it("binding 'optionsEmpty:' should provide a placeholder option value for an empty select.", function() { var $el = $(".test-select-empty"); expect( $el.children().length ).to.equal( 3 ); viewModel.set("optionsList", []); expect( $el.children().length ).to.equal( 1 ); expect( $el.find(":first-child").text() ).to.equal( "empty" ); }); it("binding 'optionsEmpty:' should bind the empty placeholder option value to a model.", function() { var $el = $(".test-select-empty"); viewModel.set("optionsList", []); viewModel.set("optEmpty", {label:"---", value:""}); expect( $el.find(":first-child").text() ).to.equal( "---" ); }); it("binding 'optionsEmpty:' should disable an empty select menu.", function() { var $el = $(".test-select-empty"); viewModel.set("optionsList", []); expect( $el.prop("disabled") ).to.equal( true ); }); it("binding 'optionsDefault:' should supersede 'optionsEmpty:' by providing a default item.", function() { var $el = $(".test-select-both"); // Empty the list, expect first option to still be the default: viewModel.set("optionsList", []); expect( $el.find(":first-child").text() ).to.equal( "default" ); // Empty the default, now expect the first option to be the empty placeholder. viewModel.set("optDefault", ""); expect( $el.find(":first-child").text() ).to.equal( "empty" ); }); it("binding 'template:' should render a bound Model with a provided template reference.", function() { var $el = $(".test-template"); }); it("binding 'template:' should render a bound Object with a provided template reference.", function() { var $el = $(".test-template"); }); it("binding 'text:' should establish a one-way binding with an element's text contents.", function() { var $el = $(".test-text-first"); expect( $el.text() ).to.equal( "Luke" ); dataModel.set("firstName", "Anakin"); expect( $el.text() ).to.equal( "Anakin" ); }); it("binding 'toggle:' should establish a one-way binding with an element's visibility.", function() { var $el = $(".test-toggle"); expect( isVisible($el) ).to.equal( true ); dataModel.set("active", false); expect( isVisible($el) ).to.equal( false ); }); it("binding 'value:' should set a value from the model into the view.", function() { var $el = $(".test-input-first"); expect( $el.val() ).to.equal( "Luke" ); }); it("binding 'value:' should set an array value from the model to a multiselect list.", function() { var $el = $(".test-select-multi"); expect( $el.val().length ).to.equal( 1 ); dataModel.set("valMulti", ["1", "2"]); expect( $el.val().length ).to.equal( 2 ); expect( $el.val().join(",") ).to.equal( "1,2" ); }); it("binding 'value:' should set a value from the view into the model.", function() { var $el = $(".test-input-first"); $el.val( "Anakin" ).trigger("change"); expect( dataModel.get("firstName") ).to.equal( "Anakin" ); }); it("operating with not() should negate a binding value.", function() { var $el = $(".test-mod-not"); expect( isVisible($el) ).to.equal( false ); dataModel.set("active", false); expect( isVisible($el) ).to.equal( true ); }); it("operating with all() should bind true when all bound values are truthy.", function() { var $el = $(".test-mod-all"); expect( $el.hasClass("hilite") ).to.equal( true ); dataModel.set("firstName", ""); expect( $el.hasClass("hilite") ).to.equal( false ); }); it("operating with none() should bind true when all bound values are falsy.", function() { var $el = $(".test-mod-none"); expect( $el.hasClass("hilite") ).to.equal( false ); dataModel.set({ firstName: "", lastName: "" }); expect( $el.hasClass("hilite") ).to.equal( true ); }); it("operating with any() should bind true when any bound value is truthy.", function() { var $el = $(".test-mod-any"); expect( $el.hasClass("hilite") ).to.equal( true ); dataModel.set("firstName", ""); expect( $el.hasClass("hilite") ).to.equal( true ); dataModel.set("lastName", ""); expect( $el.hasClass("hilite") ).to.equal( false ); }); it("operating with format() should bind true when any bound value is truthy.", function() { var $el = $(".test-mod-format"); expect( $el.text() ).to.equal( "Name: Luke Skywalker" ); dataModel.set({ firstName: "Han", lastName: "Solo" }); expect( $el.text() ).to.equal( "Name: Han Solo" ); }); it("operating with select() should perform a ternary return from three values.", function() { var $el = $(".test-mod-select"); expect( $el.text() ).to.equal( "Luke" ); dataModel.set("active", false); expect( $el.text() ).to.equal( "Skywalker" ); }); it("operating with length() should assess the length of an array/collection.", function() { var $el = $(".test-mod-length"); expect( $el.hasClass("hilite") ).to.equal( true ); viewModel.set("checkList", []); expect( $el.hasClass("hilite") ).to.equal( false ); }); });