UNPKG

can-component

Version:
422 lines (359 loc) 12.6 kB
var Component = require("can-component"); var DefineMap = require("can-define/map/map"); var QUnit = require("steal-qunit"); var SimpleMap = require("can-simple-map"); var stache = require("can-stache"); var value = require("can-value"); var globals = require("can-globals"); var Scope = require("can-view-scope"); var helpers = require("./helpers"); QUnit.module("can-component instantiation"); QUnit.test("Components can be instantiated with new", function(assert) { var ComponentConstructor = Component.extend({ tag: "new-instantiation", view: "Hello {{message}}", ViewModel: { message: "string" } }); var componentInstance = new ComponentConstructor(); var element = componentInstance.element; var viewModel = componentInstance.viewModel; // Basics look correct assert.ok(element, "instance has element property"); assert.equal(element.textContent, "Hello ", "element has correct text content"); assert.ok(viewModel, "instance has viewModel property"); // Updating the viewModel should update the element viewModel.message = "world"; assert.equal(element.textContent, "Hello world", "element has correct text content after updating viewModel"); }); QUnit.test("Components can be instantiated with <content> - no scope", function(assert) { var ComponentConstructor = Component.extend({ tag: "new-instantiation-content-no-scope", view: "Hello <content>{{message}}</content>", ViewModel: { message: {default: "world"} } }); var componentInstance = new ComponentConstructor({ content: stache("<em>mundo</em>") }); var element = componentInstance.element; // Basics look correct assert.equal(element.innerHTML, "Hello <em>mundo</em>", "content is rendered"); }); QUnit.test("Components can be instantiated with <content> - with plain content and scope", function(assert) { var ComponentConstructor = Component.extend({ tag: "new-instantiation-plain-content-and-scope", view: "Hello <content>{{message}}</content>", ViewModel: { message: {default: "world"} } }); var componentInstance = new ComponentConstructor({ content: "<em>{{message}}</em>", scope: { message: "mundo" } }); var element = componentInstance.element; // Basics look correct assert.equal(element.innerHTML, "Hello <em>mundo</em>", "content is rendered"); }); QUnit.test("Components can be instantiated with <content> - with scope - leakScope false", function(assert) { var ComponentConstructor = Component.extend({ leakScope: false, tag: "new-instantiation-content-leakscope-false", view: "Hello <content>{{message}}</content>", ViewModel: { message: {default: "world"} } }); var scopeVM = new DefineMap({}); var componentInstance = new ComponentConstructor({ content: "<em>{{message}}</em>", scope: scopeVM }); var element = componentInstance.element; // Start off without the key defined in the scope; with leakScope false, // no message will be rendered assert.equal(element.innerHTML, "Hello <em></em>", "content is rendered with the provided scope"); // Set the key in the scope; now a message will be rendered scopeVM.set("message", "mundo"); assert.equal(element.innerHTML, "Hello <em>mundo</em>", "content updates with the provided scope"); }); QUnit.test("Components can be instantiated with <content> - with scope - leakScope true", function(assert) { var ComponentConstructor = Component.extend({ leakScope: true, tag: "new-instantiation-content-leakscope-true", view: "Hello <content>{{message}}</content>", ViewModel: { message: {default: "world"} } }); var componentInstance = new ComponentConstructor({ content: "<em>{{scope.find('message')}}</em>", scope: { message: "mundo" } }); var element = componentInstance.element; // leakScope works assert.equal(element.innerHTML, "Hello <em>world</em>", "content is rendered with the component’s scope"); }); QUnit.test("Components can be instantiated with templates", function(assert) { var ComponentConstructor = Component.extend({ tag: "new-instantiation-templates", view: "<can-slot name='messageInput' />" }); var scopeVM = new DefineMap({ message: "world" }); var componentInstance = new ComponentConstructor({ scope: scopeVM, templates: { messageInput: "<input value:bind='message' />" } }); // Basics look correct var element = componentInstance.element; var inputElement = element.querySelector("input"); assert.ok(inputElement, "template rendered"); assert.equal(inputElement.value, "world", "input has correct value"); // Updating the scopeVM should update the template scopeVM.message = "mundo"; assert.equal(inputElement.value, "mundo", "input has correct value after updating scopeVM"); }); QUnit.test("Components can be instantiated with viewModel", function(assert) { // These are the observables that would typically be outside the component’s scope var bindMap = new SimpleMap({inner: new SimpleMap({key: "original bind value"})}); var fromMap = new SimpleMap({inner: new SimpleMap({key: "original from value"})}); var toMap = new SimpleMap({inner: new SimpleMap({key: "original to value"})}); // Our component var ComponentConstructor = Component.extend({ tag: "new-instantiation-viewmodel", view: "Hello", ViewModel: { fromChildProp: "string", plainProp: "string", toParentProp: "string", twoWayProp: "string", nullProp: { default: function() { return 'bar'; } } } }); // Create a new instance of our component var componentInstance = new ComponentConstructor({ // Pass the viewModel with a mix of plain and observable values viewModel: { plainProp: "plain value", fromChildProp: value.from(fromMap, "inner.key"), toParentProp: value.to(toMap, "inner.key"), twoWayProp: value.bind(bindMap, "inner.key"), nullProp: null } }); var viewModel = componentInstance.viewModel; // Initial values are correct assert.equal(viewModel.fromChildProp, "original from value", "fromChildProp init"); assert.equal(viewModel.plainProp, "plain value", "plainProp init"); assert.equal(viewModel.toParentProp, undefined, "toParentProp init"); assert.equal(viewModel.twoWayProp, "original bind value", "twoWayProp init"); assert.equal(viewModel.nullProp, null, "nullProp init"); // Updating the fromChildProp fromMap.get("inner").set("key", "new from value"); assert.equal(viewModel.fromChildProp, "new from value", "viewModel updated after fromMap set"); // Updating the toParentProp viewModel.toParentProp = "new to value"; assert.equal(toMap.get("inner").get("key"), "new to value", "toMap updated after viewModel set"); // Updating the twoWayProp bindMap.get("inner").set("key", "new bind value"); assert.equal(viewModel.twoWayProp, "new bind value", "viewModel updated after bindMap set"); viewModel.twoWayProp = "newest bind value"; assert.equal(bindMap.get("inner").get("key"), "newest bind value", "bindMap updated after viewModel set"); }); QUnit.test("Components can be instantiated with all options", function(assert) { // Our component var HelloWorld = Component.extend({ tag: "hello-world", view: "Hello <content>world</content> <ul>{{#each(items)}} <can-slot name='itemTemplate' this:from='this' /> {{/each}}</ul>", ViewModel: { items: {} } }); // Create a new instance of our component var componentInstance = new HelloWorld({ content: "<em>{{message}}</em>", scope: { message: "friend" }, templates: { itemTemplate: "<li>{{this}}</li>" }, viewModel: { items: ["eat"] } }); var element = componentInstance.element; var viewModel = componentInstance.viewModel; // Basics look correct assert.equal( helpers.cloneAndClean(element).innerHTML, "Hello <em>friend</em> <ul> <li>eat</li> </ul>", "element renders correctly" ); assert.equal(viewModel.items.length, 1, "viewModel has items"); // Changing the view model updates the element viewModel.items.push("sleep"); assert.equal( helpers.cloneAndClean(element).innerHTML, "Hello <em>friend</em> <ul> <li>eat</li> <li>sleep</li> </ul>", "element updates correctly" ); }); QUnit.test("Component binding instantiation works as documented", function(assert) { // These are the observables that would typically be outside the component’s scope var appVM = new SimpleMap({ family: new SimpleMap({ first: "Milo", last: "Flanders" }) }); // Our component var NameComponent = Component.extend({ tag: "name-component", view: "{{fullName}}", ViewModel: { givenName: "string", familyName: "string", get fullName() { return this.givenName + " " + this.familyName; } } }); // Create a new instance of our component var componentInstance = new NameComponent({ viewModel: { givenName: value.from(appVM, "family.first"), familyName: value.bind(appVM, "family.last"), fullName: value.to(appVM, "family.full") } }); var viewModel = componentInstance.viewModel; // Initial component values are correct assert.equal(viewModel.familyName, "Flanders", "component “bind” prop is correct"); assert.equal(viewModel.givenName, "Milo", "component “from” prop is correct"); assert.equal(viewModel.fullName, "Milo Flanders", "component “to” prop is correct"); // Initial map values are correct var family = appVM.get("family"); assert.equal(family.get("last"), "Flanders", "map “bind” prop is correct"); assert.equal(family.get("first"), "Milo", "map “from” prop is correct"); assert.equal(family.get("full"), "Milo Flanders", "map “to” prop is correct"); }); QUnit.test("component instantiation is not observable", function(assert) { var innerViewModel; var InnerComponent = Component.extend({ tag: "inner-component-to-make", view: "{{this.innerValue}}", ViewModel: { init: function(){ innerViewModel = this; }, innerValue: "any" } }); var count = 0; Component.extend({ tag: "outer-component-creator", view: "{{{ this.innerComponent }}}", ViewModel: { get innerComponent() { count++; return new InnerComponent({ viewModel: { innerValue: value.bind(this, "outerValue") } }); }, outerValue: "any" } }); var view = stache("<outer-component-creator/>"); view(); innerViewModel.innerValue = "SOME-VALUE"; assert.equal(count, 1, "only updated once"); }); QUnit.test("Can render in a document with no body", function(assert) { var doc = document.implementation.createHTMLDocument("Testing"); globals.setKeyValue("document", doc); doc.documentElement.removeChild(doc.body); var ComponentConstructor = Component.extend({ tag: "with-no-body", view: "test", ViewModel: { connectedCallback: function() {} } }); try { new ComponentConstructor(); assert.ok(true, "rendered without throwing"); } catch(e){ assert.ok(false, "threw" + e.toString()); } finally { globals.setKeyValue("document", document); } }); QUnit.test("{initializeBindings: false} prevents setting up bindings until insert", function(assert) { var ComponentConstructor = Component.extend({ tag: "some-random-tag", view: "{{color}}", ViewModel: { color: { default: "red" } } }); var inst = new ComponentConstructor({ initializeBindings: false }); assert.equal(inst.viewModel.color, undefined, "ViewModel not yet setup"); var view = stache("{{component}}"); var frag = view({ component: inst }); assert.equal(inst.viewModel.color, "red", "is red"); assert.equal(frag.firstChild.firstChild.data, "red", "Now it is setup"); }); QUnit.test("passing a scope-like as the component scope results in a new Scope with scope-like's context, parent, meta", function(assert) { var context = new DefineMap(); var parentScope = new Scope(); var meta = {}; var tagData = { scope: { get: function() {}, set: function() {}, peek: function() {}, computeData: function() {}, getScope: function() {}, add: function() {}, getHelperOrPartial: function() {}, getTemplateContext: function() {}, cloneFromRef: function() {}, _context: context, _parent: parentScope, _meta: meta } }; var ComponentConstructor = Component.extend({ tag: "some-random-tag", view: "{{color}}", ViewModel: { color: { default: "red" } } }); new ComponentConstructor(tagData); assert.ok(tagData.scope instanceof Scope, "scope-like is replaced with native scope"); assert.equal(tagData.scope._context, context, "new scope has same context"); assert.equal(tagData.scope._parent, parentScope, "new scope has same parent"); assert.equal(tagData.scope._meta, meta, "new scope has same meta"); });