UNPKG

can-component

Version:
620 lines (472 loc) 16 kB
var QUnit = require("steal-qunit"); var canData = require('can-dom-data'); var helpers = require("./helpers"); var SimpleMap = require("can-simple-map"); var stache = require("can-stache"); var Component = require("can-component"); var canViewModel = require('can-view-model'); var DefineMap = require('can-define/map/map'); var DefineList = require("can-define/list/list"); var Scope = require("can-view-scope"); var SetterObservable = require("can-simple-observable/setter/setter"); var SimpleObservable = require("can-simple-observable"); var canReflect = require("can-reflect"); var canSymbol = require('can-symbol'); var domEvents = require('can-dom-events'); var domMutateNode = require('can-dom-mutate/node'); var Construct = require("can-construct"); var tag = require('can-view-callbacks').tag; var innerHTML = function(el){ return el && el.innerHTML; }; helpers.makeTests("can-component viewModels", function(){ QUnit.test("a SimpleMap constructor as .ViewModel", function(assert) { var map = new SimpleMap({name: "Matthew"}); Component.extend({ tag: "can-map-viewmodel", view: stache("{{name}}"), ViewModel: function(){ return map; } }); var renderer = stache("<can-map-viewmodel></can-map-viewmodel>"); assert.equal(renderer().firstChild.firstChild.nodeValue, "Matthew"); }); QUnit.test("a SimpleMap as viewModel", function(assert) { var me = new SimpleMap({ name: "Justin" }); Component.extend({ tag: 'my-viewmodel', view: stache("{{name}}}"), viewModel: me }); var renderer = stache('<my-viewmodel></my-viewmodel>'); assert.equal(renderer().firstChild.firstChild.nodeValue, "Justin"); }); QUnit.test("a SimpleMap constructor as viewModel", function(assert) { var MyMap = SimpleMap.extend({ setup: function(props){ props.name = "Matthew"; return SimpleMap.prototype.setup.apply(this, arguments); } }); Component.extend({ tag: "can-map-viewmodel", view: stache("{{name}}"), viewModel: MyMap }); var renderer = stache("<can-map-viewmodel></can-map-viewmodel>"); assert.equal(renderer().firstChild.firstChild.nodeValue, "Matthew"); }); QUnit.test("an object is turned into a SimpleMap as viewModel", function(assert) { Component.extend({ tag: "can-map-viewmodel", view: stache("{{name}}"), viewModel: { name: "Matthew" } }); var renderer = stache("<can-map-viewmodel></can-map-viewmodel>"); var fragOne = renderer(); var vmOne = canViewModel(fragOne.firstChild); var fragTwo = renderer(); vmOne.set("name", "Wilbur"); assert.equal(fragOne.firstChild.firstChild.nodeValue, "Wilbur", "The first map changed values"); assert.equal(fragTwo.firstChild.firstChild.nodeValue, "Matthew", "The second map did not change"); }); QUnit.test("Providing viewModel and ViewModel throws", function(assert) { try { Component.extend({ tag: "viewmodel-test", view: stache("<div></div>"), viewModel: {}, ViewModel: SimpleMap.extend({}) }); assert.ok(false, "Should have thrown because we provided both"); } catch(er) { assert.ok(true, "It threw because we provided both viewModel and ViewModel"); } }); QUnit.test("canViewModel utility", function(assert) { Component({ tag: "my-taggy-tag", view: stache("<h1>hello</h1>"), viewModel: function(){ return new SimpleMap({ foo: "bar" }); } }); var frag = stache("<my-taggy-tag id='x'></my-taggy-tag>")(); var el = frag.firstChild; assert.equal(canViewModel(el), el[canSymbol.for('can.viewModel')], "one argument grabs the viewModel object"); assert.equal(canViewModel(el, "foo"), "bar", "two arguments fetches a value"); canViewModel(el, "foo", "baz"); assert.equal(canViewModel(el, "foo"), "baz", "Three arguments sets the value"); }); QUnit.test('setting passed variables - two way binding', function(assert) { Component.extend({ tag: "my-toggler", view: stache("{{#if visible}}<content/>{{/if}}"), leakScope: true, ViewModel: DefineMap.extend({ visible: {value: true}, show: function () { this.set('visible', true); }, hide: function () { this.set("visible", false); } }) }); Component.extend({ tag: "my-app", ViewModel: DefineMap.extend({ visible: {value: true}, show: function () { this.set('visible', true); } }) }); var renderer = stache("<my-app>" + '{{^visible}}<button on:click="show()">show</button>{{/visible}}' + '<my-toggler visible:bind="visible">' + 'content' + '<button on:click="hide()">hide</button>' + '</my-toggler>' + '</my-app>'); var frag = renderer({}); var myApp = frag.firstChild, buttons = myApp.getElementsByTagName("button"); assert.equal( buttons.length, 1, "there is one button"); assert.equal( innerHTML(buttons[0]) , "hide", "the button's text is hide"); domEvents.dispatch(buttons[0], "click"); buttons = myApp.getElementsByTagName("button"); assert.equal(buttons.length, 1, "there is one button"); assert.equal(innerHTML(buttons[0]), "show", "the button's text is show"); domEvents.dispatch(buttons[0], "click"); buttons = myApp.getElementsByTagName("button"); assert.equal(buttons.length, 1, "there is one button"); assert.equal(innerHTML(buttons[0]), "hide", "the button's text is hide"); }); QUnit.test("don't update computes unnecessarily", function(assert) { var sourceAge = new SimpleObservable(30), timesComputeIsCalled = 0; var age = new SetterObservable(function () { timesComputeIsCalled++; if (timesComputeIsCalled === 1) { assert.ok(true, "reading initial value to set as years"); } else if (timesComputeIsCalled === 3) { assert.ok(true, "called back another time after set to get the value"); } else { assert.ok(false, "(getter) You've called the callback " + timesComputeIsCalled + " times"); } return sourceAge.get(); }, function(newVal){ timesComputeIsCalled++; if (timesComputeIsCalled === 2) { assert.ok(true, "updating value to " + newVal); } else { assert.ok(false, "(setter) You've called the callback " + timesComputeIsCalled + " times"); } sourceAge.set(newVal); }); Component.extend({ tag: "age-er" }); var renderer = stache("<age-er years:bind='age'></age-er>"); renderer({ age: age }); age.set(31); }); QUnit.test("viewModel not rebound correctly (#550)", function(assert) { var nameChanges = 0; Component.extend({ tag: "viewmodel-rebinder", events: { "{name}": function () { nameChanges++; } } }); var renderer = stache("<viewmodel-rebinder></viewmodel-rebinder>"); var frag = renderer(); var viewModel = canViewModel(frag.firstChild); var n1 = new SimpleObservable(), n2 = new SimpleObservable(); viewModel.set("name", n1); n1.set("updated"); viewModel.set("name", n2); n2.set("updated"); assert.equal(nameChanges, 2); }); /* test("@ keeps properties live now", function () { Component.extend({ tag: "attr-fun", view: stache("<h1>{{fullName}}</h1>"), ViewModel: SimpleMap.extend({ fullName: function () { return this.get("firstName") + " " + this.attr("lastName"); } }) }); var frag = stache("<attr-fun first-name='Justin' last-name='Meyer'></attr-fun>")(); var attrFun = frag.firstChild; this.fixture.appendChild(attrFun); equal( innerHTML(attrFun.firstChild), "Justin Meyer"); attr.set(attrFun, "first-name", "Brian"); var done = assert.async(); setTimeout(function () { equal(attrFun.firstChild.firstChild.nodeValue, "Brian Meyer"); done(); }, 100); }); test('component does not update viewModel on id, class, and data-view-id attribute changes (#1079)', function(){ Component.extend({ tag:'x-app' }); var frag=stache('<x-app></x-app>')({}); var el = frag.firstChild; var viewModel = canViewModel(el); // element must be inserted, otherwise attributes event will not be fired domMutate.appendChild.call(this.fixture,frag); // update the class className.add.call(el,"foo"); var done = assert.async(); setTimeout(function(){ equal(viewModel.attr('class'),undefined, "the viewModel is not updated when the class attribute changes"); done(); }, 100); }); */ QUnit.test("id and class should work now (#694)", function(assert) { Component.extend({ tag: "stay-classy", ViewModel: SimpleMap.extend({ setup: function(props){ canReflect.assign(props, { notid: "foo", notclass: 5, notdataviewid: {} }); return SimpleMap.prototype.setup.apply(this, arguments); } }) }); var data = { idData: "id-success", classData: "class-success" }; var frag = stache( "<stay-classy id:bind='idData'" + " class:bind='classData'></stay-classy>")(data); var stayClassy = frag.firstChild; domMutateNode.appendChild.call(this.fixture, frag); var viewModel = canViewModel(stayClassy); assert.equal(viewModel.get("id"), "id-success"); assert.equal(viewModel.get("class"), "class-success"); }); QUnit.test("Construct are passed normally", function(assert) { var Constructed = Construct.extend({foo:"bar"},{}); Component.extend({ tag: "con-struct", view: stache("{{con.foo}}") }); var stached = stache("<con-struct con:bind='Constructed'></con-struct>"); var res = stached({ Constructed: Constructed }); assert.equal(innerHTML(res.firstChild), "bar"); }); QUnit.test('Component two way binding loop (#1579)', function(assert) { var changeCount = 0; Component.extend({ tag: 'product-swatch-color', viewModel: { tag: 'product-swatch-color' } }); Component.extend({ tag: 'product-swatch', view: stache('<product-swatch-color variations:bind="variations"></product-swatch-color>'), ViewModel: DefineMap.extend({ variations: { set: function(variations) { if(changeCount > 500) { return; } changeCount++; return new DefineList(variations.get()); } } }) }); var frag = stache('<product-swatch></product-swatch>')(), productSwatch = frag.firstChild; canViewModel( productSwatch ).set('variations', new DefineList()); assert.ok(changeCount < 500, "more than 500 events"); }); QUnit.test('two-way binding syntax INTRODUCED in v2.3 ALLOWS a child property to initialize an undefined parent property', function(assert) { var renderer = stache('<pa-rent/>'); Component.extend({ tag : 'pa-rent', view: stache('<chi-ld childProp:bind="parentProp" />') }); Component.extend({ tag : 'chi-ld', ViewModel: { childProp: { value: 'bar' } } }); var frag = renderer({}); var parentVM = canViewModel(frag.firstChild); var childVM = canViewModel(frag.firstChild.firstChild); assert.equal(parentVM.get("parentProp"), 'bar', 'parentProp is bar'); assert.equal(childVM.get("childProp"), 'bar', 'childProp is bar'); parentVM.set("parentProp",'foo'); assert.equal(parentVM.get("parentProp"), 'foo', 'parentProp is foo'); assert.equal(childVM.get("childProp"), 'foo', 'childProp is foo'); childVM.set("childProp",'baz'); assert.equal(parentVM.get("parentProp"), 'baz', 'parentProp is baz'); assert.equal(childVM.get("childProp"), 'baz', 'childProp is baz'); }); QUnit.test("conditional attributes (#2077)", function(assert) { Component.extend({ tag: 'some-comp', ViewModel: DefineMap.extend({ seal: false }, {}) }); var renderer = stache("<some-comp "+ "{{#if preview}}next:from='nextPage'{{/if}} "+ "swap:from='{{swapName}}' "+ "{{#preview}}checked{{/preview}} "+ "></some-comp>"); var map = new SimpleMap({ preview: true, nextPage: 2, swapName: "preview" }); var frag = renderer(map); var vm = canViewModel(frag.firstChild); var threads = [ function(){ assert.equal(vm.next, 2, "has binding initially"); assert.equal(vm.swap, true, "swap - has binding"); //equal(vm.get("checked"), "", "attr - has binding"); (commented out because we don't do this sort of binding) map.attr("preview", false); }, function(){ assert.equal(vm.swap, false, "swap - updated binidng"); //ok(vm.get("checked") === null, "attr - value set to null"); map.attr("nextPage", 3); assert.equal(vm.next, 2, "not updating after binding is torn down"); map.attr("preview", true); }, function(){ assert.equal(vm.next, 3, "re-initialized with binding"); assert.equal(vm.swap, true, "swap - updated binidng"); //equal(vm.get("checked"), "", "attr - has binding set again"); map.attr("swapName", "nextPage"); }, function(){ assert.equal(vm.swap, 3, "swap - updated binding key"); map.attr("nextPage",4); assert.equal(vm.swap, 4, "swap - updated binding"); } ]; var done = assert.async(); var index = 0; var next = function(){ if(index < threads.length) { threads[index](); index++; setTimeout(next, 150); } else { done(); } }; setTimeout(next, 100); }); QUnit.test("one-way - child to parent - parent that does not leak scope, but has no view", function(assert) { Component.extend({ tag: "outer-noleak", ViewModel: DefineMap.extend("Outer", {}, { name: { default: "outer" }, myChild: { default: null } }), leakScope: false }); Component.extend({ tag: "my-child", ViewModel : DefineMap.extend("Inner", {}, { name: { default: "inner" } }), leakScope: false }); var renderer = stache("<outer-noleak><my-child this:to='myChild'/></outer-noleak>"); var frag = renderer(); var vm = canViewModel(frag.firstChild); assert.equal(vm.myChild.name,"inner", "got instance"); }); QUnit.test("Can be called on an element using preventDataBindings (#183)", function(assert) { Component.extend({ tag: "prevent-data-bindings", ViewModel: {}, view: stache("{{value}}") }); var document = this.document; var el = document.createElement("div"); var callback = tag("prevent-data-bindings"); var vm = new DefineMap({ value: "it worked" }); el[canSymbol.for('can.viewModel')] = vm; canData.set(el, "preventDataBindings", true); callback(el, { scope: new Scope({ value: "it did not work" }) }); canData.set(el, "preventDataBindings", false); assert.equal(el.firstChild.nodeValue, "it worked"); }); QUnit.test("viewModel available as viewModel property (#282)", function(assert) { Component.extend({ tag: "can-map-viewmodel", view: stache("{{name}}"), viewModel: { name: "Matthew" } }); var renderer = stache("<can-map-viewmodel></can-map-viewmodel>"); var fragOne = renderer(); var vmOne = fragOne.firstChild.viewModel; var fragTwo = renderer(); vmOne.set("name", "Wilbur"); assert.equal(fragOne.firstChild.firstChild.nodeValue, "Wilbur", "The first map changed values"); assert.equal(fragTwo.firstChild.firstChild.nodeValue, "Matthew", "The second map did not change"); }); QUnit.test("connectedCallback without a disconnect calls stopListening", function(assert) { assert.expect(1); var done = assert.async(); var map = new SimpleMap(); Component.extend({ tag: "connected-component-listen", view: stache('rendered'), ViewModel: { connectedCallback: function(element) { this.listenTo(map,"foo", function(){}); } } }); var template = stache("<connected-component-listen/>"); var frag = template(); var first = frag.firstChild; domMutateNode.appendChild.call(this.fixture, frag); helpers.afterMutation(function(){ domMutateNode.removeChild.call(first.parentNode, first); helpers.afterMutation(function(){ assert.notOk( canReflect.isBound(map), "stopListening no matter what on vm"); done(); }); }); }); });