From aca4604879c239010c071fc2f63ca084e493ca20 Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Thu, 26 Mar 2015 17:51:08 +0100 Subject: [PATCH] feat(CSSClass): support binding to classList Closes #876 --- modules/angular2/src/directives/class.js | 37 ++++ modules/angular2/src/facade/collection.dart | 3 + modules/angular2/src/facade/collection.es6 | 1 + .../angular2/test/directives/class_spec.js | 179 ++++++++++++++++++ .../outputgeneration/DartParseTreeWriter.js | 2 +- 5 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 modules/angular2/src/directives/class.js create mode 100644 modules/angular2/test/directives/class_spec.js diff --git a/modules/angular2/src/directives/class.js b/modules/angular2/src/directives/class.js new file mode 100644 index 0000000000..69d8375cac --- /dev/null +++ b/modules/angular2/src/directives/class.js @@ -0,0 +1,37 @@ +import {Decorator} from 'angular2/src/core/annotations/annotations'; +import {isPresent} from 'angular2/src/facade/lang'; +import {DOM} from 'angular2/src/dom/dom_adapter'; +import {NgElement} from 'angular2/src/core/dom/element'; + +@Decorator({ + selector: '[class]', + bind: { + 'iterableChanges': 'class | keyValDiff' + } +}) +export class CSSClass { + _domEl; + constructor(ngEl: NgElement) { + this._domEl = ngEl.domElement; + } + + _toggleClass(className, enabled) { + if (enabled) { + DOM.addClass(this._domEl, className); + } else { + DOM.removeClass(this._domEl, className); + } + } + + set iterableChanges(changes) { + if (isPresent(changes)) { + changes.forEachAddedItem((record) => { this._toggleClass(record.key, record.currentValue); }); + changes.forEachChangedItem((record) => { this._toggleClass(record.key, record.currentValue); }); + changes.forEachRemovedItem((record) => { + if (record.previousValue) { + DOM.removeClass(this._domEl, record.key); + } + }); + } + } +} diff --git a/modules/angular2/src/facade/collection.dart b/modules/angular2/src/facade/collection.dart index 6fe8aa9a9c..3918ae256e 100644 --- a/modules/angular2/src/facade/collection.dart +++ b/modules/angular2/src/facade/collection.dart @@ -69,6 +69,9 @@ class StringMapWrapper { static void set(Map map, key, value) { map[key] = value; } + static void delete(Map m, k) { + m.remove(k); + } static void forEach(Map m, fn(v, k)) { m.forEach((k, v) => fn(v, k)); } diff --git a/modules/angular2/src/facade/collection.es6 b/modules/angular2/src/facade/collection.es6 index 30504cb605..98ecb5c551 100644 --- a/modules/angular2/src/facade/collection.es6 +++ b/modules/angular2/src/facade/collection.es6 @@ -62,6 +62,7 @@ export class StringMapWrapper { } return true; } + static delete(map, key) { delete map[key]; } static forEach(map, callback) { for (var prop in map) { if (map.hasOwnProperty(prop)) { diff --git a/modules/angular2/test/directives/class_spec.js b/modules/angular2/test/directives/class_spec.js new file mode 100644 index 0000000000..d752a1b06b --- /dev/null +++ b/modules/angular2/test/directives/class_spec.js @@ -0,0 +1,179 @@ +import { + AsyncTestCompleter, + beforeEach, + beforeEachBindings, + ddescribe, + xdescribe, + describe, + el, + expect, + iit, + inject, + it, + xit, + } from 'angular2/test_lib'; + +import {StringMapWrapper} from 'angular2/src/facade/collection'; + +import {Injector, bind} from 'angular2/di'; + +import {Compiler} from 'angular2/src/core/compiler/compiler'; +import {TemplateResolver} from 'angular2/src/core/compiler/template_resolver'; + +import {Template} from 'angular2/src/core/annotations/template'; +import {Decorator, Component} from 'angular2/src/core/annotations/annotations'; + +import {MockTemplateResolver} from 'angular2/src/mock/template_resolver_mock'; + +import {CSSClass} from 'angular2/src/directives/class'; + +export function main() { + describe('binding to CSS class list', () => { + + var view, cd, compiler, component, tplResolver; + + beforeEachBindings(() => [ + bind(TemplateResolver).toClass(MockTemplateResolver) + ]); + + beforeEach(inject([Compiler, TemplateResolver], (c, t) => { + compiler = c; + tplResolver = t; + })); + + function createView(pv) { + component = new TestComponent(); + view = pv.instantiate(null, null); + view.hydrate(new Injector([]), null, null, component, null); + cd = view.changeDetector; + } + + function compileWithTemplate(html) { + var template = new Template({ + inline: html, + directives: [CSSClass] + }); + tplResolver.setTemplate(TestComponent, template); + return compiler.compile(TestComponent); + } + + it('should add classes specified in an object literal', inject([AsyncTestCompleter], (async) => { + var template = '
'; + compileWithTemplate(template).then((pv) => { + createView(pv); + + cd.detectChanges(); + expect(view.nodes[0].className).toEqual('ng-binding foo'); + + async.done(); + }); + })); + + it('should add and remove classes based on changes in object literal values', inject([AsyncTestCompleter], (async) => { + var template = '
'; + compileWithTemplate(template).then((pv) => { + createView(pv); + cd.detectChanges(); + expect(view.nodes[0].className).toEqual('ng-binding foo'); + + component.condition = false; + cd.detectChanges(); + expect(view.nodes[0].className).toEqual('ng-binding bar'); + + async.done(); + }); + })); + + it('should add and remove classes based on changes to the expression object', inject([AsyncTestCompleter], (async) => { + var template = '
'; + compileWithTemplate(template).then((pv) => { + createView(pv); + cd.detectChanges(); + expect(view.nodes[0].className).toEqual('ng-binding foo'); + + StringMapWrapper.set(component.expr, 'bar', true); + cd.detectChanges(); + expect(view.nodes[0].className).toEqual('ng-binding foo bar'); + + StringMapWrapper.set(component.expr, 'baz', true); + cd.detectChanges(); + expect(view.nodes[0].className).toEqual('ng-binding foo bar baz'); + + StringMapWrapper.delete(component.expr, 'bar'); + cd.detectChanges(); + expect(view.nodes[0].className).toEqual('ng-binding foo baz'); + + async.done(); + }); + })); + + it('should retain existing classes when expression evaluates to null', inject([AsyncTestCompleter], (async) => { + var template = '
'; + compileWithTemplate(template).then((pv) => { + createView(pv); + cd.detectChanges(); + expect(view.nodes[0].className).toEqual('ng-binding foo'); + + component.expr = null; + cd.detectChanges(); + expect(view.nodes[0].className).toEqual('ng-binding foo'); + + component.expr = {'foo': false, 'bar': true}; + cd.detectChanges(); + expect(view.nodes[0].className).toEqual('ng-binding bar'); + + async.done(); + }); + })); + + it('should co-operate with the class attribute', inject([AsyncTestCompleter], (async) => { + var template = '
'; + compileWithTemplate(template).then((pv) => { + createView(pv); + + StringMapWrapper.set(component.expr, 'bar', true); + cd.detectChanges(); + expect(view.nodes[0].className).toEqual('init foo ng-binding bar'); + + StringMapWrapper.set(component.expr, 'foo', false); + cd.detectChanges(); + expect(view.nodes[0].className).toEqual('init ng-binding bar'); + + async.done(); + }); + })); + + it('should co-operate with the class attribute and class.name binding', inject([AsyncTestCompleter], (async) => { + var template = '
'; + compileWithTemplate(template).then((pv) => { + createView(pv); + cd.detectChanges(); + expect(view.nodes[0].className).toEqual('init foo ng-binding baz'); + + StringMapWrapper.set(component.expr, 'bar', true); + cd.detectChanges(); + expect(view.nodes[0].className).toEqual('init foo ng-binding baz bar'); + + StringMapWrapper.set(component.expr, 'foo', false); + cd.detectChanges(); + expect(view.nodes[0].className).toEqual('init ng-binding baz bar'); + + component.condition = false; + cd.detectChanges(); + expect(view.nodes[0].className).toEqual('init ng-binding bar'); + + async.done(); + }); + })); + }) +} + +@Component({selector: 'test-cmp'}) +class TestComponent { + condition:boolean; + expr; + constructor() { + this.condition = true; + this.expr = {'foo': true, 'bar': false}; + } +} diff --git a/tools/transpiler/src/outputgeneration/DartParseTreeWriter.js b/tools/transpiler/src/outputgeneration/DartParseTreeWriter.js index cb37617a87..69771eb6fc 100644 --- a/tools/transpiler/src/outputgeneration/DartParseTreeWriter.js +++ b/tools/transpiler/src/outputgeneration/DartParseTreeWriter.js @@ -531,4 +531,4 @@ export class DartParseTreeWriter extends JavaScriptParseTreeWriter { } // see: https://www.dartlang.org/docs/dart-up-and-running/ch02.html for a full list. -const DART_RESERVED_WORDS = ['if', 'switch', 'for']; +const DART_RESERVED_WORDS = ['if', 'switch', 'for', 'class'];