test('two-way binding syntax INTRODUCED in v2.3 ALLOWS a child property to initialize an undefined parent property', function(){ 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); equal(parentVM.get("parentProp"), 'bar', 'parentProp is bar'); equal(childVM.get("childProp"), 'bar', 'childProp is bar'); parentVM.set("parentProp",'foo'); equal(parentVM.get("parentProp"), 'foo', 'parentProp is foo'); equal(childVM.get("childProp"), 'foo', 'childProp is foo'); childVM.set("childProp",'baz'); equal(parentVM.get("parentProp"), 'baz', 'parentProp is baz'); equal(childVM.get("childProp"), 'baz', 'childProp is baz'); });
test('one way - child to parent - importing viewModel hyphenatedProp:to="test"', function(){ MockComponent.extend({ tag: 'import-prop-scope', template: stache('Hello {{userName}}'), viewModel: { userName: '******', age: 7, updateName: function(){ this.set('userName', 'Justin'); } } }); MockComponent.extend({ tag: 'import-prop-parent', template: stache('<import-prop-scope vm:userName:to="test" vm:this:to="childComponent"></import-prop-scope>' + '<div>Imported: {{test}}</div>') }); var template = stache('<import-prop-parent></import-prop-parent>'); var frag = template({}); var importPropParent = frag.firstChild; var importPropScope = importPropParent.getElementsByTagName("import-prop-scope")[0]; canViewModel(importPropScope).updateName(); var importPropParentViewModel = canViewModel(importPropParent); equal(importPropParentViewModel.get("test"), "Justin", "got hyphenated prop"); equal(importPropParentViewModel.get("childComponent"), canViewModel(importPropScope), "got view model"); });
test('Should configure chart using a passed config', 1, (assert) => { let tpl = '<bit-c3 {config}="config"><bit-c3-data><bit-c3-data-column /></bit-c3-data></bit-c3>'; let frag = stache(tpl)({ config: { axis: { x: { type: 'category', tick: { rotate: -45, multiline: false }, height: 130 } } } }); let vm = canViewModel(frag.querySelector('bit-c3')); assert.deepEqual(vm.config, { data: { columns:[], x:'x' }, axis: { x: { type:'category', tick: { rotate: -45, multiline: false }, height: 130 } }, bindto: undefined }, 'Config object is defined correctly'); });
test("viewModel not rebound correctly (#550)", function () { 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"); equal(nameChanges, 2); });
test("one-way pass computes to components with ~", function(assert) { expect(6); MockComponent.extend({ tag: "foo-bar" }); var baseVm = new SimpleMap({foo : "bar"}); this.fixture.appendChild(stache("<foo-bar compute:from=\"~foo\"></foo-bar>")(baseVm)); var vm = canViewModel(this.fixture.firstChild); ok(vm.get("compute")[canSymbol.for('can.getValue')], "observable returned"); equal(vm.get("compute")(), "bar", "Compute has correct value"); canReflect.onValue(vm.get("compute"), function() { // NB: This gets called twice below, once by // the parent and once directly. ok(true, "Change handler called"); }); baseVm.set("foo", "quux"); equal(vm.get("compute")(), "quux", "Compute updates"); vm.get("compute")("xyzzy"); equal(baseVm.get("foo"), "xyzzy", "Compute does update the other direction"); });
test("id and class should work now (#694)", function () { 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); equal(viewModel.get("id"), "id-success"); equal(viewModel.get("class"), "class-success"); });
test("function reference to child (#2116)", function(){ expect(2); var template = stache('<foo-bar vm:child:from="parent"></foo-bar>'); MockComponent.extend({ tag : 'foo-bar', viewModel : { method: function(){ ok(false, "should not be called"); } } }); var VM = SimpleMap.extend({ parent : function() { ok(false, "should not be called"); } }); var vm = new VM({}); var frag = template(vm); equal( typeof canViewModel(frag.firstChild).attr("child"), "function", "to child binding"); template = stache('<foo-bar vm:method:to="vmMethod"></foo-bar>'); vm = new VM({}); template(vm); ok(typeof vm.attr("vmMethod") === "function", "parent export function"); });
test('one-way - child to parent - viewModel - with converters', function(){ MockComponent.extend({ tag: "view-model-able", viewModel: function(){ return new SimpleMap({viewModelProp: "Mercury"}); } }); stache.addConverter("upper-case", { get: function( fooCompute ) { return (""+canReflect.getValue(fooCompute)).toUpperCase(); }, set: function( newVal, fooCompute ) { canReflect.setValue(fooCompute, (""+newVal).toUpperCase() ); } }); var template = stache("<view-model-able vm:viewModelProp:to='upper-case(scopeProp)'/>"); var map = new SimpleMap({scopeProp: "Venus"}); var frag = template(map); var viewModel = canViewModel(frag.firstChild); equal( viewModel.get("viewModelProp"), "Mercury", "initial value kept" ); equal( map.get("scopeProp"), "MERCURY", "initial value set on parent, but upper cased" ); viewModel.set("viewModelProp", "Earth"); equal(map.get("scopeProp"), "EARTH", "binding from child to parent updated"); map.set("scopeProp", "Mars"); equal( viewModel.get("viewModelProp"), "Earth", "no binding from parent to child" ); });
var handler = function(ev) { var attrVal = el.getAttribute(encoder.encode(attributeName)); if (!attrVal) { return; } var viewModel = canViewModel(el); // expression.parse will read the attribute // value and parse it identically to how mustache helpers // get parsed. var expr = expression.parse(attrVal, { lookupRule: function() { return expression.Lookup; }, methodRule: "call" }); var runScope = makeScopeFromEvent(el, ev, viewModel, arguments, data, bindingContext); if (expr instanceof expression.Hashes) { var hashExprs = expr.hashExprs; var key = Object.keys(hashExprs)[0]; var value = expr.hashExprs[key].value(runScope); var isObservableValue = canReflect.isObservableLike(value) && canReflect.isValueLike(value); runScope.set(key, isObservableValue ? canReflect.getValue(value) : value); } else if (expr instanceof expression.Call) { runEventCallback(el, ev, data, runScope, expr, attributeName, attrVal); } else { throw new Error("can-stache-bindings: Event bindings must be a call expression. Make sure you have a () in " + data.attributeName + "=" + JSON.stringify(attrVal)); } };
test("two way - viewModel (#1700)", function(){ var template = stache("<div vm:viewModelProp:bind='scopeProp'/>"); var map = new SimpleMap({ scopeProp: "Hello" }); var scopeMapSetCalled = 0; // overwrite setKeyValue to catch child->parent updates var origMapSetKeyValue = map[canSymbol.for("can.setKeyValue")]; map[canSymbol.for("can.setKeyValue")] = function(attrName, value){ if(typeof attrName === "string" && arguments.length > 1) { scopeMapSetCalled++; } return origMapSetKeyValue.apply(this, arguments); }; // RENDER var frag = template(map); var viewModel = canViewModel(frag.firstChild); equal(scopeMapSetCalled, 0, "set is not called on scope map"); equal(viewModel.get("viewModelProp"), "Hello", "initial value set" ); viewModel = canViewModel(frag.firstChild); var viewModelSetCalled = 1; // set once already - on "initial value set" var origViewModelSet = viewModel[canSymbol.for("can.setKeyValue")]; viewModel[canSymbol.for("can.setKeyValue")] = function(attrName){ if(typeof attrName === "string" && arguments.length > 1) { viewModelSetCalled++; } return origViewModelSet.apply(this, arguments); }; viewModel.set("viewModelProp", "HELLO"); equal(map.get("scopeProp"), "HELLO", "binding from child to parent"); equal(scopeMapSetCalled, 1, "set is called on scope map"); equal(viewModelSetCalled, 2, "set is called viewModel"); map.set("scopeProp", "WORLD"); equal(viewModel.get("viewModelProp"), "WORLD", "binding from parent to child" ); equal(scopeMapSetCalled, 1, "can.setKey is not called again on scope map"); equal(viewModelSetCalled, 3, "set is called again on viewModel"); });
test('two-way - reference - child:bind="scope.vars.ref" (#1700)', function(){ var data = new SimpleMap({person: new SimpleMap({name: new SimpleMap({})}) }); MockComponent.extend({ tag: 'reference-export', viewModel: function(){ return new SimpleMap({tag: 'reference-export'}); } }); MockComponent.extend({ tag: 'ref-import', viewModel: function(){ return new SimpleMap({tag: 'ref-import'}); } }); var template = stache("<reference-export name:bind='scope.vars.refName'/>"+ "<ref-import name:bind='scope.vars.refName'/> {{helperToGetScope()}}"); var scope; var frag = template(data,{ helperToGetScope: function(options){ scope = options.scope; } }); var refExport = canViewModel(frag.firstChild); var refImport = canViewModel(frag.firstChild.nextSibling); refExport.set("name", "v1"); equal( scope.peek("scope.vars.refName"), "v1", "reference scope updated"); equal(refImport.get("name"), "v1", "updated ref-import"); refImport.set("name", "v2"); equal(refExport.get("name"), "v2", "updated ref-export"); equal( scope.peek("scope.vars.refName"), "v2", "actually put in refs scope"); });
test("canViewModel utility", function() { 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; equal(canViewModel(el), el[canSymbol.for('can.viewModel')], "one argument grabs the viewModel object"); equal(canViewModel(el, "foo"), "bar", "two arguments fetches a value"); canViewModel(el, "foo", "baz"); equal(canViewModel(el, "foo"), "baz", "Three arguments sets the value"); });
test("standard attributes should not set viewModel props", function(){ MockComponent.extend({ tag: "test-elem", viewModel: SimpleMap }); var template = stache("<test-elem foo=\"bar\"/>"); var frag = template(new SimpleMap({ bar: true })); var vm = canViewModel(frag.firstChild); equal(vm.get('foo'), undefined); });
test('one-way - parent to child - viewModel', function(){ var template = stache("<div vm:viewModelProp:from='scopeProp'/>"); var map = new SimpleMap({scopeProp: "Venus"}); var frag = template(map); var viewModel = canViewModel(frag.firstChild); equal( viewModel.attr("viewModelProp"), "Venus", "initial value set" ); viewModel.attr("viewModelProp", "Earth"); equal(map.attr("scopeProp"), "Venus", "no binding from child to parent"); map.attr("scopeProp", "Mars"); equal( viewModel.attr("viewModelProp"), "Mars", "binding from parent to child" ); });
QUnit.test("ViewModel properties default to DefineList if set to an Array (#225)", function() { Component.extend({ tag: "viewmodel-lists", view: "Hello, World", ViewModel: { items: { default: function() { return [ "one", "two" ]; } } } }); var renderer = stache("<viewmodel-lists></viewmodel-lists>"); var fragOne = renderer(); var vm = viewModel(fragOne.firstChild); QUnit.ok(vm.items instanceof define.DefineList, 'vm is a DefineList'); });
QUnit.test("an object is turned into a SimpleMap as viewModel", function() { 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"); equal(fragOne.firstChild.firstChild.nodeValue, "Wilbur", "The first map changed values"); equal(fragTwo.firstChild.firstChild.nodeValue, "Matthew", "The second map did not change"); });
QUnit.test('Works with can-define', function () { var VM = define.Constructor({ firstName: { type: 'string' }, lastName: { type: 'string' }, fullName: { get: function () { return [this.firstName, this.lastName].join(' '); } } }); Component.extend({ tag: 'can-define-component', ViewModel: VM, view: stache('Name: {{fullName}}') }); var frag = stache('<can-define-component firstName:from="firstName" lastName:from="lastName" />')({ firstName: 'Chris', lastName: 'Gomez' }); var vm = viewModel(frag.firstChild); QUnit.ok(vm instanceof VM, 'Constructor was called'); QUnit.equal(vm.firstName, 'Chris', 'ViewModel was set from scope'); QUnit.equal(vm.lastName, 'Gomez', 'ViewModel was set from scope'); QUnit.equal(frag.firstChild.innerHTML, 'Name: Chris Gomez', 'Rendered fullName'); vm.firstName = 'Justin'; vm.lastName = 'Meyer'; QUnit.equal(frag.firstChild.innerHTML, 'Name: Justin Meyer', 'Rendered fullName after change'); });
test('Component two way binding loop (#1579)', function() { 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()); ok(changeCount < 500, "more than 500 events"); });
QUnit.test("ViewModel defaults to DefineMap if set to an Object", function() { Component.extend({ tag: 'can-define-component', ViewModel: { firstName: { type: 'string' }, lastName: { type: 'string' }, fullName: { get: function () { return [this.firstName, this.lastName].join(' '); } } }, view: stache('Name: {{fullName}}') }); var frag = stache('<can-define-component firstName:from="firstName" lastName:from="lastName" />')({ firstName: 'Chris', lastName: 'Gomez' }); var vm = viewModel(frag.firstChild); QUnit.ok(vm instanceof DefineMap, 'vm is a DefineMap'); QUnit.equal(vm.firstName, 'Chris', 'ViewModel was set from scope'); QUnit.equal(vm.lastName, 'Gomez', 'ViewModel was set from scope'); QUnit.equal(frag.firstChild.innerHTML, 'Name: Chris Gomez', 'Rendered fullName'); vm.firstName = 'Justin'; vm.lastName = 'Meyer'; QUnit.equal(frag.firstChild.innerHTML, 'Name: Justin Meyer', 'Rendered fullName after change'); });
test('one-way - child to parent - viewModel', function(){ MockComponent.extend({ tag: "view-model-able", viewModel: function(){ return new SimpleMap({viewModelProp: "Mercury"}); } }); var template = stache("<view-model-able vm:viewModelProp:to='scopeProp'/>"); var map = new SimpleMap({scopeProp: "Venus"}); var frag = template(map); var viewModel = canViewModel(frag.firstChild); equal( viewModel.get("viewModelProp"), "Mercury", "initial value kept" ); equal( map.get("scopeProp"), "Mercury", "initial value set on parent" ); viewModel.set("viewModelProp", "Earth"); equal(map.get("scopeProp"), "Earth", "binding from child to parent"); map.set("scopeProp", "Mars"); equal( viewModel.get("viewModelProp"), "Earth", "no binding from parent to child" ); });
QUnit.test("one-way - child to parent - parent that does not leak scope, but has no view", function(){ 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); QUnit.equal(vm.myChild.name,"inner", "got instance"); });
getViewModel = ObservationRecorder.ignore(function() { return viewModel || (viewModel = canViewModel(el)); }),
inserted: function(viewModel, ev) { this.viewModel.chart = canViewModel(this.element.parentElement).chart; this.viewModel.updateName(); },
export const LFeatureLayer = Component.extend({ tag: 'l-feature-layer', ViewModel: DefineMap.extend({ url: 'string', _layer: '*', _map: { set (map) { this._layer = new FeatureLayer({url: this.url}); this._layer.addTo(map); console.log(map); return map; } }, destroy () { console.log('Dstroying!!'); this._map.removeLayer(this._layer); this._layer = null; this._map = null; } }), events: { inserted (element) { debugger; const lmap = canViewModel(element.parentElement); if (lmap && lmap.mapObject) { this.viewModel._map = lmap.mapObject; } } } });
inserted: function () { this.viewModel.parent = canViewModel(this.element.parentNode); if (this.viewModel.parent && this.viewModel.parent.addPage) { this.viewModel.parent.addPage(this.viewModel); } },
testIfRealDocument("Bi-directional binding among sibling components, new syntax (#325)", function () { var groupCollapsed = console.groupCollapsed; if(groupCollapsed) { console.groupCollapsed = null; //no op } var demoContext = new DefineMap({ person: '' }); var SourceComponentVM = DefineMap.extend("SourceComponentVM",{ defaultPerson: { value: 'John' }, person: { set: function(val) { return val || this.defaultPerson; } } }); var ClearComponentVM = DefineMap.extend("ClearComponentVM",{ person: 'string', clearPerson: function() { this.set('person', ''); } }); MockComponent.extend({ tag: "source-component", viewModel: SourceComponentVM, template: stache('<span>{{person}}</span><input type="text" value:bind="./person" />') }); MockComponent.extend({ tag: "clear-button", viewModel: ClearComponentVM, template: stache('<input type="button" value="Clear" on:click="./clearPerson()" /><span>{{./person}}</span>') }); var demoRenderer = stache( '<span>{{./person}}</span>' + '<source-component person:bind="./person" />' + '<clear-button person:bind="./person" />' ); var frag = demoRenderer(demoContext); var sourceComponentVM = canViewModel(frag.childNodes[1]); var clearButtonVM = canViewModel(frag.childNodes[2]); QUnit.equal(frag.childNodes[0].childNodes[0].nodeValue, '', "demoContext person is empty"); QUnit.equal(frag.childNodes[1].childNodes[0].childNodes[0].nodeValue, 'John', "source-component person is default"); QUnit.equal(frag.childNodes[2].childNodes[1].childNodes[0].nodeValue, '', "clear-button person is empty"); sourceComponentVM.person = 'Bob'; QUnit.equal(frag.childNodes[0].childNodes[0].nodeValue, 'Bob', "demoContext person set correctly"); QUnit.equal(frag.childNodes[1].childNodes[0].childNodes[0].nodeValue, 'Bob', "source-component person set correctly"); QUnit.equal(frag.childNodes[2].childNodes[1].childNodes[0].nodeValue, 'Bob', "clear-button person set correctly"); clearButtonVM.clearPerson(); // Note that 'John' will not be set on the parent or clear button because parent was already set // to an empty string and the bindingSemaphore will not allow another change to the parent // (giving the parent priority) to prevent cyclic dependencies. QUnit.equal(frag.childNodes[0].childNodes[0].nodeValue, '', "demoContext person set correctly"); QUnit.equal(frag.childNodes[1].childNodes[0].childNodes[0].nodeValue, 'John', "source-component person set correctly"); QUnit.equal(frag.childNodes[2].childNodes[1].childNodes[0].nodeValue, '', "clear-button person set correctly"); if(groupCollapsed) { console.groupCollapsed = groupCollapsed; } });
$.fn.viewModel = function(){ return canViewModel(this[0]); };
this.visible = false; this.hideAll(); } return false; }, /** * Queries the dom for other dropdown-menu components and hides them. * This is used when a dropdown component is visible and another one is * clicked, any others will be made invisible * @function hideAll * @signature `hideAll()` */ hideAll () { const nodes = document.querySelectorAll('dropdown-menu'); for (let i = 0; i < nodes.length; i ++) { const vm = canViewModel(nodes[i]); if (vm.visible) { vm.visible = false; } } }, /** * When a primary button is clicked, this function dispatches the `primaryclick` * event with the button that was clicked as its argument. * @function onPrimaryClick * @param {ButtonObject} button the button that was clicked * @param {MouseEvent} event The mouse click event on the button that we should prevent default * @return {Boolean} returns false to prevent event from changing page route */ onPrimaryClick (button, event) { if (event) {
hidePopup () { this.overlay.setPosition(undefined); this.modalActive = false; } }); assign(ViewModel.prototype, CanEvent); /** * @module {can.Component} ol-popup <ol-popup /> * @parent geo.components */ export const OlPopup = Component.extend({ tag: 'ol-popup', view: template, ViewModel: ViewModel, leakScope: true, events: { inserted () { var mapViewModel = canViewModel(this.element.parentNode); this.viewModel.overlayElement = this.element.querySelector('.ol-popup'); this.viewModel.map = mapViewModel.mapObject; }, removed () { if (this.viewModel.overlay && this.viewModel.map) { this.viewModel.map.removeOverlay(this.overlay); } } } });
test("conditional attributes (#2077)", function(){ 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(){ equal(vm.next, 2, "has binding initially"); 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(){ equal(vm.swap, false, "swap - updated binidng"); //ok(vm.get("checked") === null, "attr - value set to null"); map.attr("nextPage", 3); equal(vm.next, 2, "not updating after binding is torn down"); map.attr("preview", true); }, function(){ equal(vm.next, 3, "re-initialized with binding"); equal(vm.swap, true, "swap - updated binidng"); //equal(vm.get("checked"), "", "attr - has binding set again"); map.attr("swapName", "nextPage"); }, function(){ equal(vm.swap, 3, "swap - updated binding key"); map.attr("nextPage",4); equal(vm.swap, 4, "swap - updated binding"); } ]; stop(); var index = 0; var next = function(){ if(index < threads.length) { threads[index](); index++; setTimeout(next, 150); } else { start(); } }; setTimeout(next, 100); });