diff --git a/packages/ember-glimmer/lib/utils/references.js b/packages/ember-glimmer/lib/utils/references.js index 1261d5001f3..5af7f02cada 100644 --- a/packages/ember-glimmer/lib/utils/references.js +++ b/packages/ember-glimmer/lib/utils/references.js @@ -333,8 +333,23 @@ export class AttributeBindingReference extends CachedReference { compute() { let value = get(this.component, this.propertyPath); - if (value === null || value === undefined) { + if (value === null || value === undefined || value === false) { return null; + } else if (value === true) { + // Note: + // This is here to mimic functionality in HTMLBars for properties. + // For instance when a property like "disable" is set all of these + // forms are valid and have the same disabled functionality: + // + // + // + // + // + // + // For compatability sake we do not just cast the true boolean to + // a string. Potentially we can revisit this in the future as the + // casting feels better and we can remove this branch. + return ''; } else { return value; } diff --git a/packages/ember-glimmer/tests/integration/components/attribute-bindings-test.js b/packages/ember-glimmer/tests/integration/components/attribute-bindings-test.js new file mode 100644 index 00000000000..9d3d6f8adae --- /dev/null +++ b/packages/ember-glimmer/tests/integration/components/attribute-bindings-test.js @@ -0,0 +1,473 @@ +import { moduleFor, RenderingTest } from '../../utils/test-case'; +import { Component } from '../../utils/helpers'; +import { strip } from '../../utils/abstract-test-case'; +import { set } from 'ember-metal/property_set'; +import { observersFor } from 'ember-metal/observer'; + + +moduleFor('Attribute bindings integration', class extends RenderingTest { + ['@test it can have attribute bindings']() { + let FooBarComponent = Component.extend({ + attributeBindings: ['foo:data-foo', 'bar:data-bar'] + }); + + this.registerComponent('foo-bar', { ComponentClass: FooBarComponent, template: 'hello' }); + + this.render('{{foo-bar foo=foo bar=bar}}', { foo: 'foo', bar: 'bar' }); + + this.assertComponentElement(this.firstChild, { tagName: 'div', attrs: { 'data-foo': 'foo', 'data-bar': 'bar' }, content: 'hello' }); + + this.runTask(() => this.rerender()); + + this.assertComponentElement(this.firstChild, { tagName: 'div', attrs: { 'data-foo': 'foo', 'data-bar': 'bar' }, content: 'hello' }); + + this.runTask(() => { + set(this.context, 'foo', 'FOO'); + set(this.context, 'bar', undefined); + }); + + this.assertComponentElement(this.firstChild, { tagName: 'div', attrs: { 'data-foo': 'FOO' }, content: 'hello' }); + + this.runTask(() => { + set(this.context, 'foo', 'foo'); + set(this.context, 'bar', 'bar'); + }); + + this.assertComponentElement(this.firstChild, { tagName: 'div', attrs: { 'data-foo': 'foo', 'data-bar': 'bar' }, content: 'hello' }); + } + + ['@test handles non-microsyntax attributeBindings']() { + let FooBarComponent = Component.extend({ + attributeBindings: ['type'] + }); + + this.registerComponent('foo-bar', { ComponentClass: FooBarComponent, template: 'hello' }); + + this.render('{{foo-bar type=submit}}', { + submit: 'submit' + }); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { type: 'submit' }, content: 'hello' }); + + this.runTask(() => this.rerender()); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { type: 'submit' }, content: 'hello' }); + + this.runTask(() => set(this.context, 'submit', 'password')); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { type: 'password' }, content: 'hello' }); + + this.runTask(() => set(this.context, 'submit', 'submit')); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { type: 'submit' }, content: 'hello' }); + } + + ['@glimmer normalizes attributeBinding names']() { + let FooBarComponent = Component.extend({ + attributeBindings: ['disAbled'] + }); + + this.registerComponent('foo-bar', { ComponentClass: FooBarComponent, template: 'hello' }); + + this.render('{{foo-bar disAbled=bool}}', { + bool: true + }); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { disabled: '' }, content: 'hello' }); + + this.runTask(() => this.rerender()); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { disabled: '' }, content: 'hello' }); + + this.runTask(() => set(this.context, 'bool', null)); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: {}, content: 'hello' }); + + this.runTask(() => set(this.context, 'bool', true)); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { disabled: '' }, content: 'hello' }); + } + + ['@htmlbars normalizes attributeBinding names']() { + let FooBarComponent = Component.extend({ + attributeBindings: ['disAbled'] + }); + + this.registerComponent('foo-bar', { ComponentClass: FooBarComponent, template: 'hello' }); + + this.render('{{foo-bar disAbled=bool}}', { + bool: true + }); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { disabled: 'true' }, content: 'hello' }); + + this.runTask(() => this.rerender()); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { disabled: 'true' }, content: 'hello' }); + + this.runTask(() => set(this.context, 'bool', null)); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: {}, content: 'hello' }); + + this.runTask(() => set(this.context, 'bool', true)); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { disabled: 'true' }, content: 'hello' }); + } + + ['@test attributeBindings handles null/undefined']() { + let FooBarComponent = Component.extend({ + attributeBindings: ['fizz', 'bar'] + }); + + this.registerComponent('foo-bar', { ComponentClass: FooBarComponent, template: 'hello' }); + + this.render('{{foo-bar fizz=fizz bar=bar}}', { + fizz: null, + bar: undefined + }); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: {}, content: 'hello' }); + + this.runTask(() => this.rerender()); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: {}, content: 'hello' }); + + this.runTask(() => { + set(this.context, 'fizz', 'fizz'); + set(this.context, 'bar', 'bar'); + }); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { fizz: 'fizz', bar: 'bar' }, content: 'hello' }); + + this.runTask(() => { + set(this.context, 'fizz', null); + set(this.context, 'bar', undefined); + }); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: {}, content: 'hello' }); + } + + ['@test attributeBindings handles number value']() { + let FooBarComponent = Component.extend({ + attributeBindings: ['size'] + }); + + this.registerComponent('foo-bar', { ComponentClass: FooBarComponent, template: 'hello' }); + + this.render('{{foo-bar size=size}}', { + size: 21 + }); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { size: '21' }, content: 'hello' }); + + this.runTask(() => this.rerender()); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { size: '21' }, content: 'hello' }); + + this.runTask(() => set(this.context, 'size', 0)); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { size: '0' }, content: 'hello' }); + + this.runTask(() => set(this.context, 'size', 21)); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { size: '21' }, content: 'hello' }); + } + + ['@test handles internal and external changes']() { + let component; + let FooBarComponent = Component.extend({ + attributeBindings: ['type'], + type: 'password', + init() { + this._super(...arguments); + component = this; + } + }); + + this.registerComponent('foo-bar', { ComponentClass: FooBarComponent, template: 'hello' }); + + this.render('{{foo-bar}}'); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { type: 'password' }, content: 'hello' }); + + this.runTask(() => this.rerender()); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { type: 'password' }, content: 'hello' }); + + this.runTask(() => set(component, 'type', 'checkbox')); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { type: 'checkbox' }, content: 'hello' }); + + this.runTask(() => set(component, 'type', 'password')); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { type: 'password' }, content: 'hello' }); + } + + ['@test can set attributeBindings on component with a different tagName']() { + let FooBarComponent = Component.extend({ + tagName: 'input', + attributeBindings: ['type', 'isDisabled:disabled'] + }); + + this.registerComponent('foo-bar', { ComponentClass: FooBarComponent }); + + this.render('{{foo-bar type=type isDisabled=disabled}}', { + type: 'password', + disabled: false + }); + + this.assertComponentElement(this.nthChild(0), { tagName: 'input', attrs: { type: 'password' } }); + + this.runTask(() => this.rerender()); + + this.assertComponentElement(this.nthChild(0), { tagName: 'input', attrs: { type: 'password' } }); + + this.runTask(() => { + set(this.context, 'type', 'checkbox'); + set(this.context, 'disabled', true); + }); + + this.assertComponentElement(this.nthChild(0), { tagName: 'input', attrs: { type: 'checkbox', disabled: '' } }); + + this.runTask(() => { + set(this.context, 'type', 'password'); + set(this.context, 'disabled', false); + }); + + this.assertComponentElement(this.nthChild(0), { tagName: 'input', attrs: { type: 'password' } }); + } + + ['@test should allow namespaced attributes in micro syntax']() { + let FooBarComponent = Component.extend({ + attributeBindings: ['xlinkHref:xlink:href'] + }); + + this.registerComponent('foo-bar', { ComponentClass: FooBarComponent }); + + this.render('{{foo-bar type=type xlinkHref=xlinkHref}}', { + xlinkHref: '/foo.png' + }); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { 'xlink:href': '/foo.png' } }); + + this.runTask(() => this.rerender()); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { 'xlink:href': '/foo.png' } }); + + this.runTask(() => set(this.context, 'xlinkHref', '/lol.png')); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { 'xlink:href': '/lol.png' } }); + + this.runTask(() => set(this.context, 'xlinkHref', '/foo.png')); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { 'xlink:href': '/foo.png' } }); + } + + // This comes into play when using the {{#each}} helper. If the + // passed array item is a String, it will be converted into a + // String object instead of a normal string. + ['@test should allow for String objects']() { + let FooBarComponent = Component.extend({ + attributeBindings: ['foo'] + }); + + this.registerComponent('foo-bar', { ComponentClass: FooBarComponent }); + + this.render('{{foo-bar foo=foo}}', { + foo: (function() { return this; }).call('bar') + }); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { 'foo': 'bar' } }); + + this.runTask(() => this.rerender()); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { 'foo': 'bar' } }); + + this.runTask(() => set(this.context, 'foo', (function() { return this; }).call('baz'))); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { 'foo': 'baz' } }); + + this.runTask(() => set(this.context, 'foo', (function() { return this; }).call('bar'))); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { 'foo': 'bar' } }); + } + + // Bug in both systems. Should not be able to update id post creation. + ['@skip can set id initially via attributeBindings ']() { + let FooBarComponent = Component.extend({ + attributeBindings: ['specialSauce:id'] + }); + + this.registerComponent('foo-bar', { ComponentClass: FooBarComponent }); + + this.render('{{foo-bar specialSauce=sauce}}', { + sauce: 'special-sauce' + }); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { 'id': 'special-sauce' } }); + + this.runTask(() => this.rerender()); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { 'id': 'special-sauce' } }); + + this.runTask(() => set(this.context, 'sauce', 'foo')); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { 'id': 'special-sauce' } }); + } + + ['@htmlbars attributeBindings are overwritten']() { + let FooBarComponent = Component.extend({ + attributeBindings: ['href'], + href: 'a href' + }); + + let FizzBarComponent = FooBarComponent.extend({ + attributeBindings: ['newHref:href'] + }); + + this.registerComponent('fizz-bar', { ComponentClass: FizzBarComponent }); + + this.render('{{fizz-bar newHref=href}}', { + href: 'dog.html' + }); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { href: 'dog.html' } }); + + this.runTask(() => this.rerender()); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { href: 'dog.html' } }); + + this.runTask(() => set(this.context, 'href', 'cat.html')); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { href: 'cat.html' } }); + } + + ['@htmlbars should teardown observers'](assert) { + let component; + let FooBarComponent = Component.extend({ + attributeBindings: ['foo'], + foo: 'bar', + init() { + this._super(...arguments); + component = this; + } + }); + + this.registerComponent('foo-bar', { ComponentClass: FooBarComponent }); + + this.render('{{foo-bar}}'); + + assert.equal(observersFor(component, 'foo').length, 1); + + this.runTask(() => this.rerender()); + + assert.equal(observersFor(component, 'foo').length, 1); + } + + ['@test it can set attribute bindings in the constructor']() { + let FooBarComponent = Component.extend({ + init() { + this._super(); + + let bindings = []; + + if (this.get('hasFoo')) { + bindings.push('foo:data-foo'); + } + + if (this.get('hasBar')) { + bindings.push('bar:data-bar'); + } + + this.attributeBindings = bindings; + } + }); + + this.registerComponent('foo-bar', { ComponentClass: FooBarComponent, template: 'hello' }); + + this.render(strip` + {{foo-bar hasFoo=true foo=foo hasBar=false bar=bar}} + {{foo-bar hasFoo=false foo=foo hasBar=true bar=bar}} + {{foo-bar hasFoo=true foo=foo hasBar=true bar=bar}} + {{foo-bar hasFoo=false foo=foo hasBar=false bar=bar}} + `, { foo: 'foo', bar: 'bar' }); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { 'data-foo': 'foo' }, content: 'hello' }); + this.assertComponentElement(this.nthChild(1), { tagName: 'div', attrs: { 'data-bar': 'bar' }, content: 'hello' }); + this.assertComponentElement(this.nthChild(2), { tagName: 'div', attrs: { 'data-foo': 'foo', 'data-bar': 'bar' }, content: 'hello' }); + this.assertComponentElement(this.nthChild(3), { tagName: 'div', attrs: { }, content: 'hello' }); + + this.runTask(() => this.rerender()); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { 'data-foo': 'foo' }, content: 'hello' }); + this.assertComponentElement(this.nthChild(1), { tagName: 'div', attrs: { 'data-bar': 'bar' }, content: 'hello' }); + this.assertComponentElement(this.nthChild(2), { tagName: 'div', attrs: { 'data-foo': 'foo', 'data-bar': 'bar' }, content: 'hello' }); + this.assertComponentElement(this.nthChild(3), { tagName: 'div', attrs: { }, content: 'hello' }); + + this.runTask(() => { + set(this.context, 'foo', 'FOO'); + set(this.context, 'bar', undefined); + }); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { 'data-foo': 'FOO' }, content: 'hello' }); + this.assertComponentElement(this.nthChild(1), { tagName: 'div', attrs: { }, content: 'hello' }); + this.assertComponentElement(this.nthChild(2), { tagName: 'div', attrs: { 'data-foo': 'FOO' }, content: 'hello' }); + this.assertComponentElement(this.nthChild(3), { tagName: 'div', attrs: { }, content: 'hello' }); + + this.runTask(() => set(this.context, 'bar', 'BAR')); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { 'data-foo': 'FOO' }, content: 'hello' }); + this.assertComponentElement(this.nthChild(1), { tagName: 'div', attrs: { 'data-bar': 'BAR' }, content: 'hello' }); + this.assertComponentElement(this.nthChild(2), { tagName: 'div', attrs: { 'data-foo': 'FOO', 'data-bar': 'BAR' }, content: 'hello' }); + this.assertComponentElement(this.nthChild(3), { tagName: 'div', attrs: { }, content: 'hello' }); + + this.runTask(() => { + set(this.context, 'foo', 'foo'); + set(this.context, 'bar', 'bar'); + }); + + this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { 'data-foo': 'foo' }, content: 'hello' }); + this.assertComponentElement(this.nthChild(1), { tagName: 'div', attrs: { 'data-bar': 'bar' }, content: 'hello' }); + this.assertComponentElement(this.nthChild(2), { tagName: 'div', attrs: { 'data-foo': 'foo', 'data-bar': 'bar' }, content: 'hello' }); + this.assertComponentElement(this.nthChild(3), { tagName: 'div', attrs: { }, content: 'hello' }); + } + + ['@test it should not allow attributeBindings to be set']() { + this.registerComponent('foo-bar', { template: 'hello' }); + + expectAssertion(() => { + this.render('{{foo-bar attributeBindings="one two"}}'); + }, /Setting 'attributeBindings' via template helpers is not allowed/); + } + + ['@test asserts if an attributeBinding is setup on class']() { + let FooBarComponent = Component.extend({ + tagName: 'div', + attributeBindings: ['class'] + }); + + this.registerComponent('foo-bar', { ComponentClass: FooBarComponent, template: 'hello' }); + + expectAssertion(() => { + this.render('{{foo-bar}}'); + }, /You cannot use class as an attributeBinding, use classNameBindings instead./i); + } + + ['@htmlbars blacklists href bindings based on protocol']() { + /* jshint scripturl:true */ + + let FooBarComponent = Component.extend({ + tagName: 'a', + attributeBindings: ['href'] + }); + + this.registerComponent('foo-bar', { ComponentClass: FooBarComponent, template: 'hello' }); + + this.render('{{foo-bar href=xss}}', { + xss: 'javascript:alert(\'foo\')' + }); + + this.assertComponentElement(this.nthChild(0), { tagName: 'a', attrs: { href: 'unsafe:javascript:alert(\'foo\')' } }); + } + +}); diff --git a/packages/ember-glimmer/tests/integration/components/curly-components-test.js b/packages/ember-glimmer/tests/integration/components/curly-components-test.js index a12e8bb0e64..b917cfb629f 100644 --- a/packages/ember-glimmer/tests/integration/components/curly-components-test.js +++ b/packages/ember-glimmer/tests/integration/components/curly-components-test.js @@ -289,112 +289,6 @@ moduleFor('Components test: curly components', class extends RenderingTest { this.assertComponentElement(this.nthChild(2), { tagName: 'div', attrs: { 'class': classes('ember-view foo') }, content: 'hello' }); } - ['@test it can have attribute bindings']() { - let FooBarComponent = Component.extend({ - attributeBindings: ['foo:data-foo', 'bar:data-bar'] - }); - - this.registerComponent('foo-bar', { ComponentClass: FooBarComponent, template: 'hello' }); - - this.render('{{foo-bar foo=foo bar=bar}}', { foo: 'foo', bar: 'bar' }); - - this.assertComponentElement(this.firstChild, { tagName: 'div', attrs: { 'data-foo': 'foo', 'data-bar': 'bar' }, content: 'hello' }); - - this.runTask(() => this.rerender()); - - this.assertComponentElement(this.firstChild, { tagName: 'div', attrs: { 'data-foo': 'foo', 'data-bar': 'bar' }, content: 'hello' }); - - this.runTask(() => { - set(this.context, 'foo', 'FOO'); - set(this.context, 'bar', undefined); - }); - - this.assertComponentElement(this.firstChild, { tagName: 'div', attrs: { 'data-foo': 'FOO' }, content: 'hello' }); - - this.runTask(() => { - set(this.context, 'foo', 'foo'); - set(this.context, 'bar', 'bar'); - }); - - this.assertComponentElement(this.firstChild, { tagName: 'div', attrs: { 'data-foo': 'foo', 'data-bar': 'bar' }, content: 'hello' }); - } - - ['@test it can set attribute bindings in the constructor']() { - let FooBarComponent = Component.extend({ - init() { - this._super(); - - let bindings = []; - - if (this.get('hasFoo')) { - bindings.push('foo:data-foo'); - } - - if (this.get('hasBar')) { - bindings.push('bar:data-bar'); - } - - this.attributeBindings = bindings; - } - }); - - this.registerComponent('foo-bar', { ComponentClass: FooBarComponent, template: 'hello' }); - - this.render(strip` - {{foo-bar hasFoo=true foo=foo hasBar=false bar=bar}} - {{foo-bar hasFoo=false foo=foo hasBar=true bar=bar}} - {{foo-bar hasFoo=true foo=foo hasBar=true bar=bar}} - {{foo-bar hasFoo=false foo=foo hasBar=false bar=bar}} - `, { foo: 'foo', bar: 'bar' }); - - this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { 'data-foo': 'foo' }, content: 'hello' }); - this.assertComponentElement(this.nthChild(1), { tagName: 'div', attrs: { 'data-bar': 'bar' }, content: 'hello' }); - this.assertComponentElement(this.nthChild(2), { tagName: 'div', attrs: { 'data-foo': 'foo', 'data-bar': 'bar' }, content: 'hello' }); - this.assertComponentElement(this.nthChild(3), { tagName: 'div', attrs: { }, content: 'hello' }); - - this.runTask(() => this.rerender()); - - this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { 'data-foo': 'foo' }, content: 'hello' }); - this.assertComponentElement(this.nthChild(1), { tagName: 'div', attrs: { 'data-bar': 'bar' }, content: 'hello' }); - this.assertComponentElement(this.nthChild(2), { tagName: 'div', attrs: { 'data-foo': 'foo', 'data-bar': 'bar' }, content: 'hello' }); - this.assertComponentElement(this.nthChild(3), { tagName: 'div', attrs: { }, content: 'hello' }); - - this.runTask(() => { - set(this.context, 'foo', 'FOO'); - set(this.context, 'bar', undefined); - }); - - this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { 'data-foo': 'FOO' }, content: 'hello' }); - this.assertComponentElement(this.nthChild(1), { tagName: 'div', attrs: { }, content: 'hello' }); - this.assertComponentElement(this.nthChild(2), { tagName: 'div', attrs: { 'data-foo': 'FOO' }, content: 'hello' }); - this.assertComponentElement(this.nthChild(3), { tagName: 'div', attrs: { }, content: 'hello' }); - - this.runTask(() => set(this.context, 'bar', 'BAR')); - - this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { 'data-foo': 'FOO' }, content: 'hello' }); - this.assertComponentElement(this.nthChild(1), { tagName: 'div', attrs: { 'data-bar': 'BAR' }, content: 'hello' }); - this.assertComponentElement(this.nthChild(2), { tagName: 'div', attrs: { 'data-foo': 'FOO', 'data-bar': 'BAR' }, content: 'hello' }); - this.assertComponentElement(this.nthChild(3), { tagName: 'div', attrs: { }, content: 'hello' }); - - this.runTask(() => { - set(this.context, 'foo', 'foo'); - set(this.context, 'bar', 'bar'); - }); - - this.assertComponentElement(this.nthChild(0), { tagName: 'div', attrs: { 'data-foo': 'foo' }, content: 'hello' }); - this.assertComponentElement(this.nthChild(1), { tagName: 'div', attrs: { 'data-bar': 'bar' }, content: 'hello' }); - this.assertComponentElement(this.nthChild(2), { tagName: 'div', attrs: { 'data-foo': 'foo', 'data-bar': 'bar' }, content: 'hello' }); - this.assertComponentElement(this.nthChild(3), { tagName: 'div', attrs: { }, content: 'hello' }); - } - - ['@test it should not allow attributeBindings to be set']() { - this.registerComponent('foo-bar', { template: 'hello' }); - - expectAssertion(() => { - this.render('{{foo-bar attributeBindings="one two"}}'); - }, /Setting 'attributeBindings' via template helpers is not allowed/); - } - ['@test it has an element']() { let instance; diff --git a/packages/ember-views/tests/views/view/attribute_bindings_test.js b/packages/ember-views/tests/views/view/attribute_bindings_test.js deleted file mode 100644 index e19ae96b93f..00000000000 --- a/packages/ember-views/tests/views/view/attribute_bindings_test.js +++ /dev/null @@ -1,479 +0,0 @@ -import { context } from 'ember-environment'; -import run from 'ember-metal/run_loop'; -import { observersFor } from 'ember-metal/observer'; -import { changeProperties } from 'ember-metal/property_events'; -import { SafeString } from 'ember-htmlbars/utils/string'; - -import EmberView from 'ember-views/views/view'; - -let originalLookup = context.lookup; -let lookup, view; - -let appendView = function() { - run(function() { view.appendTo('#qunit-fixture'); }); -}; - -QUnit.module('EmberView - Attribute Bindings', { - setup() { - context.lookup = lookup = {}; - }, - teardown() { - if (view) { - run(function() { - view.destroy(); - }); - view = null; - } - context.lookup = originalLookup; - } -}); - -QUnit.test('should render attribute bindings', function() { - view = EmberView.create({ - attributeBindings: ['type', 'destroyed', 'exists', 'nothing', 'notDefined', 'notNumber', 'explosions'], - - type: 'submit', - exists: true, - nothing: null, - notDefined: undefined - }); - - run(function() { - view.createElement(); - }); - - equal(view.$().attr('type'), 'submit', 'updates type attribute'); - ok(view.$().attr('exists'), 'adds exists attribute when true'); - ok(!view.$().attr('nothing'), 'removes nothing attribute when null'); - equal(view.$().attr('notDefined'), undefined, 'removes notDefined attribute when undefined'); -}); - -QUnit.test('should normalize case for attribute bindings', function() { - view = EmberView.create({ - tagName: 'input', - attributeBindings: ['disAbled'], - disAbled: true - }); - - run(function() { - view.createElement(); - }); - - ok(view.$().prop('disabled'), 'sets property with correct case'); -}); - -QUnit.test('should render attribute bindings on input', function() { - view = EmberView.create({ - tagName: 'input', - attributeBindings: ['type', 'isDisabled:disabled'], - - type: 'submit', - isDisabled: true - }); - - run(function() { - view.createElement(); - }); - - equal(view.$().attr('type'), 'submit', 'updates type attribute'); - ok(view.$().prop('disabled'), 'supports customizing attribute name for Boolean values'); -}); - -QUnit.test('should update attribute bindings', function() { - view = EmberView.create({ - attributeBindings: ['type', 'color:data-color', 'exploded', 'collapsed', 'times'], - type: 'reset', - color: 'red', - exploded: 'bang', - collapsed: null, - times: 15 - }); - - run(function() { - view.createElement(); - }); - - equal(view.$().attr('type'), 'reset', 'adds type attribute'); - equal(view.$().attr('data-color'), 'red', 'attr value set with ternary'); - equal(view.$().attr('exploded'), 'bang', 'adds exploded attribute when it has a value'); - ok(!view.$().attr('collapsed'), 'does not add null attribute'); - equal(view.$().attr('times'), '15', 'sets an integer to an attribute'); - - run(function() { - view.set('type', 'submit'); - view.set('color', 'blue'); - view.set('exploded', null); - view.set('collapsed', 'swish'); - view.set('times', 16); - }); - - equal(view.$().attr('type'), 'submit', 'adds type attribute'); - equal(view.$().attr('data-color'), 'blue', 'attr value set with ternary'); - ok(!view.$().attr('exploded'), 'removed exploded attribute when it is null'); - ok(view.$().attr('collapsed'), 'swish', 'adds an attribute when it has a value'); - equal(view.$().attr('times'), '16', 'updates an integer attribute'); -}); - -QUnit.test('should update attribute bindings on input (boolean)', function() { - view = EmberView.create({ - tagName: 'input', - attributeBindings: ['disabled'], - disabled: true - }); - - run(function() { - view.createElement(); - }); - - ok(view.$().prop('disabled'), 'adds disabled property when true'); - - run(function() { - view.set('disabled', false); - }); - - ok(!view.$().prop('disabled'), 'updates disabled property when false'); -}); - -QUnit.test('should update attribute bindings on input (raw number prop)', function() { - view = EmberView.create({ - tagName: 'input', - attributeBindings: ['size'], - size: 20 - }); - - run(function() { - view.createElement(); - }); - - equal(view.$().prop('size'), 20, 'adds size property'); - - run(function() { - view.set('size', 10); - }); - - equal(view.$().prop('size'), 10, 'updates size property'); -}); - -QUnit.test('should update attribute bindings on input (name)', function() { - view = EmberView.create({ - tagName: 'input', - attributeBindings: ['name'], - name: 'bloody-awful' - }); - - run(function() { - view.createElement(); - }); - - equal(view.$().prop('name'), 'bloody-awful', 'adds name property'); - - run(function() { - view.set('name', 'simply-grand'); - }); - - equal(view.$().prop('name'), 'simply-grand', 'updates name property'); -}); - -QUnit.test('should update attribute bindings with micro syntax', function() { - view = EmberView.create({ - tagName: 'input', - attributeBindings: ['isDisabled:disabled'], - type: 'reset', - isDisabled: true - }); - - run(function() { - view.createElement(); - }); - ok(view.$().prop('disabled'), 'adds disabled property when true'); - - run(function() { - view.set('isDisabled', false); - }); - ok(!view.$().prop('disabled'), 'updates disabled property when false'); -}); - -QUnit.test('should allow namespaced attributes in micro syntax', function () { - view = EmberView.create({ - attributeBindings: ['xlinkHref:xlink:href'], - xlinkHref: '/foo.png' - }); - - run(function() { - view.createElement(); - }); - equal(view.$().attr('xlink:href'), '/foo.png', 'namespaced attribute is set'); - - run(function () { - view.set('xlinkHref', '/bar.png'); - }); - equal(view.$().attr('xlink:href'), '/bar.png', 'namespaced attribute is updated'); -}); - -QUnit.test('should update attribute bindings on svg', function() { - view = EmberView.create({ - attributeBindings: ['viewBox'], - viewBox: null - }); - - run(function() { - view.createElement(); - }); - - equal(view.$().attr('viewBox'), null, 'viewBox can be null'); - - run(function() { - view.set('viewBox', '0 0 100 100'); - }); - - equal(view.$().attr('viewBox'), '0 0 100 100', 'viewBox can be updated'); -}); - -// This comes into play when using the {{#each}} helper. If the -// passed array item is a String, it will be converted into a -// String object instead of a normal string. -QUnit.test('should allow binding to String objects', function() { - view = EmberView.create({ - attributeBindings: ['foo'], - // JSHint doesn't like `new String` so we'll create it the same way it gets created in practice - foo: (function() { return this; }).call('bar') - }); - - run(function() { - view.createElement(); - }); - - - equal(view.$().attr('foo'), 'bar', 'should convert String object to bare string'); - - run(function() { - view.set('foo', null); - }); - - ok(!view.$().attr('foo'), 'removes foo attribute when null'); -}); - -QUnit.test('should teardown observers on rerender', function() { - view = EmberView.create({ - attributeBindings: ['foo'], - classNameBindings: ['foo'], - foo: 'bar' - }); - - appendView(); - - equal(observersFor(view, 'foo').length, 1, 'observer count after render is one'); - - run(function() { - view.rerender(); - }); - - equal(observersFor(view, 'foo').length, 1, 'observer count after rerender remains one'); -}); - -QUnit.test('handles attribute bindings for properties', function() { - view = EmberView.create({ - tagName: 'input', - attributeBindings: ['checked'], - checked: null - }); - - appendView(); - - equal(!!view.$().prop('checked'), false, 'precond - is not checked'); - - run(function() { - view.set('checked', true); - }); - - equal(view.$().prop('checked'), true, 'changes to checked'); - - run(function() { - view.set('checked', false); - }); - - equal(!!view.$().prop('checked'), false, 'changes to unchecked'); -}); - -QUnit.test('handles `undefined` value for properties', function() { - view = EmberView.create({ - tagName: 'input', - attributeBindings: ['value'], - value: 'test' - }); - - appendView(); - - equal(view.$().prop('value'), 'test', 'value is defined'); - - run(function() { - view.set('value', undefined); - }); - - equal(view.$().prop('value'), '', 'value is blank'); -}); - -QUnit.test('handles null value for attributes on text fields', function() { - view = EmberView.create({ - tagName: 'input', - attributeBindings: ['value'] - }); - - appendView(); - - view.$().attr('value', 'test'); - - equal(view.$().attr('value'), 'test', 'value is defined'); - - run(function() { - view.set('value', null); - }); - - equal(!!view.$().prop('value'), false, 'value is not defined'); -}); - -QUnit.test('handles a 0 value attribute on text fields', function() { - view = EmberView.create({ - tagName: 'input', - attributeBindings: ['value'] - }); - - appendView(); - - view.$().attr('value', 'test'); - equal(view.$().attr('value'), 'test', 'value is defined'); - - run(function() { - view.set('value', 0); - }); - strictEqual(view.$().prop('value'), '0', 'value should be 0'); -}); - -QUnit.test('attributeBindings should not fail if view has been removed', function() { - run(function() { - view = EmberView.create({ - attributeBindings: ['checked'], - checked: true - }); - }); - run(function() { - view.createElement(); - }); - var error; - try { - run(function() { - changeProperties(function() { - view.set('checked', false); - view.remove(); - }); - }); - } catch(e) { - error = e; - } - ok(!error, error); -}); - -QUnit.test('attributeBindings should not fail if view has been destroyed', function() { - run(function() { - view = EmberView.create({ - attributeBindings: ['checked'], - checked: true - }); - }); - run(function() { - view.createElement(); - }); - var error; - try { - run(function() { - changeProperties(function() { - view.set('checked', false); - view.destroy(); - }); - }); - } catch(e) { - error = e; - } - ok(!error, error); -}); - -QUnit.test('asserts if an attributeBinding is setup on class', function() { - view = EmberView.create({ - attributeBindings: ['class'] - }); - - expectAssertion(function() { - appendView(); - }, 'You cannot use class as an attributeBinding, use classNameBindings instead.'); - - // Remove render node to avoid "Render node exists without concomitant env" - // assertion on teardown. - view._renderNode = null; -}); - -QUnit.test('blacklists href bindings based on protocol', function() { - /* jshint scripturl:true */ - - view = EmberView.create({ - tagName: 'a', - attributeBindings: ['href'], - href: 'javascript:alert(\'foo\')' - }); - - appendView(); - - equal(view.$().attr('href'), 'unsafe:javascript:alert(\'foo\')', 'value property sanitized'); - - run(function() { - view.set('href', new SafeString(view.get('href'))); - }); - - equal(view.$().attr('href'), 'javascript:alert(\'foo\')', 'value is not defined'); -}); - -QUnit.test('attributeBindings should be overridable', function() { - var ParentView = EmberView.extend({ - attributeBindings: ['href'], - href: 'an href' - }); - - var ChildView = ParentView.extend({ - attributeBindings: ['newHref:href'], - newHref: 'a new href' - }); - - view = ChildView.create(); - - appendView(); - - equal(view.$().attr('href'), 'a new href', 'expect value from subclass attribute binding'); -}); - -QUnit.test('role attribute is included if provided as ariaRole', function() { - view = EmberView.create({ - ariaRole: 'main' - }); - - appendView(); - - equal(view.$().attr('role'), 'main'); -}); - -QUnit.test('role attribute is not included if not provided', function() { - view = EmberView.create(); - - appendView(); - - ok(!view.element.hasAttribute('role'), 'role attribute is not present'); -}); - -QUnit.test('can set id initially via attributeBindings', function() { - view = EmberView.create({ - attributeBindings: ['specialSauce:id'], - specialSauce: 'special-sauces-id' - }); - - appendView(); - - equal(view.$().attr('id'), 'special-sauces-id', 'id properly used from attributeBindings'); -});