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');
-});