From 8c993dca037c801d70ef6aade370b7c7668ec0f6 Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Sun, 21 Jun 2015 11:54:21 +0200 Subject: [PATCH] feat(CSSClass): add support for string and array expresions Closes #2025 --- modules/angular2/src/directives/class.ts | 67 +++-- .../angular2/test/directives/class_spec.ts | 255 +++++++++++++----- 2 files changed, 242 insertions(+), 80 deletions(-) diff --git a/modules/angular2/src/directives/class.ts b/modules/angular2/src/directives/class.ts index a0fd65261b4e8..53a1330f7c784 100644 --- a/modules/angular2/src/directives/class.ts +++ b/modules/angular2/src/directives/class.ts @@ -1,40 +1,71 @@ import {Directive, onCheck} from 'angular2/annotations'; import {ElementRef} from 'angular2/core'; import {PipeRegistry} from 'angular2/src/change_detection/pipes/pipe_registry'; -import {isPresent} from 'angular2/src/facade/lang'; +import {Pipe} from 'angular2/src/change_detection/pipes/pipe'; import {Renderer} from 'angular2/src/render/api'; +import {KeyValueChanges} from 'angular2/src/change_detection/pipes/keyvalue_changes'; +import {IterableChanges} from 'angular2/src/change_detection/pipes/iterable_changes'; +import {isPresent, isString, StringWrapper} from 'angular2/src/facade/lang'; +import {ListWrapper, StringMapWrapper, isListLikeIterable} from 'angular2/src/facade/collection'; @Directive({selector: '[class]', lifecycle: [onCheck], properties: ['rawClass: class']}) export class CSSClass { - _pipe; + _pipe: Pipe; _rawClass; constructor(private _pipeRegistry: PipeRegistry, private _ngEl: ElementRef, private _renderer: Renderer) {} set rawClass(v) { - this._rawClass = v; - this._pipe = this._pipeRegistry.get('keyValDiff', this._rawClass); - } + this._cleanupClasses(this._rawClass); - _toggleClass(className, enabled): void { - this._renderer.setElementClass(this._ngEl, className, enabled); + if (isString(v)) { + v = v.split(' '); + } + + this._rawClass = v; + this._pipe = this._pipeRegistry.get(isListLikeIterable(v) ? 'iterableDiff' : 'keyValDiff', v); } - onCheck() { + onCheck(): void { var diff = this._pipe.transform(this._rawClass); - if (isPresent(diff)) this._applyChanges(diff.wrapped); + if (isPresent(diff) && isPresent(diff.wrapped)) { + if (diff.wrapped instanceof IterableChanges) { + this._applyArrayChanges(diff.wrapped); + } else { + this._applyObjectChanges(diff.wrapped); + } + } } - private _applyChanges(diff) { - if (isPresent(diff)) { - diff.forEachAddedItem((record) => { this._toggleClass(record.key, record.currentValue); }); - diff.forEachChangedItem((record) => { this._toggleClass(record.key, record.currentValue); }); - diff.forEachRemovedItem((record) => { - if (record.previousValue) { - this._toggleClass(record.key, false); - } - }); + private _cleanupClasses(rawClassVal): void { + if (isPresent(rawClassVal)) { + if (isListLikeIterable(rawClassVal)) { + ListWrapper.forEach(rawClassVal, (className) => { this._toggleClass(className, false); }); + } else { + StringMapWrapper.forEach(rawClassVal, (expVal, className) => { + if (expVal) this._toggleClass(className, false); + }); + } } } + + private _applyObjectChanges(diff: KeyValueChanges): void { + diff.forEachAddedItem((record) => { this._toggleClass(record.key, record.currentValue); }); + diff.forEachChangedItem((record) => { this._toggleClass(record.key, record.currentValue); }); + diff.forEachRemovedItem((record) => { + if (record.previousValue) { + this._toggleClass(record.key, false); + } + }); + } + + private _applyArrayChanges(diff: IterableChanges): void { + diff.forEachAddedItem((record) => { this._toggleClass(record.item, true); }); + diff.forEachRemovedItem((record) => { this._toggleClass(record.item, false); }); + } + + private _toggleClass(className: string, enabled): void { + this._renderer.setElementClass(this._ngEl, className, enabled); + } } diff --git a/modules/angular2/test/directives/class_spec.ts b/modules/angular2/test/directives/class_spec.ts index 697f9406fb27e..9411dac1784bf 100644 --- a/modules/angular2/test/directives/class_spec.ts +++ b/modules/angular2/test/directives/class_spec.ts @@ -12,105 +12,238 @@ import { it, xit, } from 'angular2/test_lib'; - -import {StringMapWrapper} from 'angular2/src/facade/collection'; - +import {List, ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection'; import {Component, View} from 'angular2/angular2'; - import {TestBed} from 'angular2/src/test_lib/test_bed'; - +import {DOM} from 'angular2/src/dom/dom_adapter'; import {CSSClass} from 'angular2/src/directives/class'; export function main() { describe('binding to CSS class list', () => { - it('should add classes specified in an object literal', - inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => { - var template = '
'; + describe('expressions evaluating to objects', () => { - tb.createView(TestComponent, {html: template}) - .then((view) => { - view.detectChanges(); - expect(view.rootNodes[0].className).toEqual('ng-binding foo'); + it('should add classes specified in an object literal', + inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => { + var template = '
'; - async.done(); - }); - })); + tb.createView(TestComponent, {html: template}) + .then((view) => { + view.detectChanges(); + expect(view.rootNodes[0].className).toEqual('ng-binding foo'); - it('should add and remove classes based on changes in object literal values', - inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => { - var template = '
'; + async.done(); + }); + })); - tb.createView(TestComponent, {html: template}) - .then((view) => { - view.detectChanges(); - expect(view.rootNodes[0].className).toEqual('ng-binding foo'); + it('should add and remove classes based on changes in object literal values', + inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => { + var template = '
'; - view.context.condition = false; - view.detectChanges(); - expect(view.rootNodes[0].className).toEqual('ng-binding bar'); + tb.createView(TestComponent, {html: template}) + .then((view) => { + view.detectChanges(); + expect(view.rootNodes[0].className).toEqual('ng-binding foo'); - async.done(); - }); - })); + view.context.condition = false; + view.detectChanges(); + expect(view.rootNodes[0].className).toEqual('ng-binding bar'); + + async.done(); + }); + })); + + it('should add and remove classes based on changes to the expression object', + inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => { + var template = '
'; + + tb.createView(TestComponent, {html: template}) + .then((view) => { + view.detectChanges(); + expect(view.rootNodes[0].className).toEqual('ng-binding foo'); + + StringMapWrapper.set(view.context.objExpr, 'bar', true); + view.detectChanges(); + expect(view.rootNodes[0].className).toEqual('ng-binding foo bar'); + + StringMapWrapper.set(view.context.objExpr, 'baz', true); + view.detectChanges(); + expect(view.rootNodes[0].className).toEqual('ng-binding foo bar baz'); + + StringMapWrapper.delete(view.context.objExpr, 'bar'); + view.detectChanges(); + expect(view.rootNodes[0].className).toEqual('ng-binding foo baz'); + + async.done(); + }); + })); + + it('should add and remove classes based on reference changes to the expression object', + inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => { + var template = '
'; + + tb.createView(TestComponent, {html: template}) + .then((view) => { + view.detectChanges(); + expect(view.rootNodes[0].className).toEqual('ng-binding foo'); + + view.context.objExpr = {foo: true, bar: true}; + view.detectChanges(); + expect(view.rootNodes[0].className).toEqual('ng-binding foo bar'); + + view.context.objExpr = {baz: true}; + view.detectChanges(); + expect(view.rootNodes[0].className).toEqual('ng-binding baz'); + + async.done(); + }); + })); + }); - it('should add and remove classes based on changes to the expression object', + describe('expressions evaluating to lists', () => { + + it('should add classes specified in a list literal', + inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => { + var template = `
`; + + tb.createView(TestComponent, {html: template}) + .then((view) => { + view.detectChanges(); + expect(view.rootNodes[0].className).toEqual('ng-binding foo bar'); + + async.done(); + }); + })); + + it('should add and remove classes based on changes to the expression', + inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => { + var template = '
'; + + tb.createView(TestComponent, {html: template}) + .then((view) => { + + var arrExpr: List = view.context.arrExpr; + + view.detectChanges(); + expect(view.rootNodes[0].className).toEqual('ng-binding foo'); + + arrExpr.push('bar'); + view.detectChanges(); + expect(view.rootNodes[0].className).toEqual('ng-binding foo bar'); + + arrExpr[1] = 'baz'; + view.detectChanges(); + expect(view.rootNodes[0].className).toEqual('ng-binding foo baz'); + + ListWrapper.remove(view.context.arrExpr, 'baz'); + view.detectChanges(); + expect(view.rootNodes[0].className).toEqual('ng-binding foo'); + + async.done(); + }); + })); + + it('should add and remove classes when a reference changes', + inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => { + var template = '
'; + + tb.createView(TestComponent, {html: template}) + .then((view) => { + view.detectChanges(); + expect(view.rootNodes[0].className).toEqual('ng-binding foo'); + + view.context.arrExpr = ['bar']; + view.detectChanges(); + expect(view.rootNodes[0].className).toEqual('ng-binding bar'); + + async.done(); + }); + })); + }); + + describe('expressions evaluating to string', () => { + + it('should add classes specified in a string literal', + inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => { + var template = `
`; + + tb.createView(TestComponent, {html: template}) + .then((view) => { + view.detectChanges(); + expect(view.rootNodes[0].className).toEqual('ng-binding foo bar'); + + async.done(); + }); + })); + + it('should add and remove classes based on changes to the expression', + inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => { + var template = '
'; + + tb.createView(TestComponent, {html: template}) + .then((view) => { + view.detectChanges(); + expect(view.rootNodes[0].className).toEqual('ng-binding foo'); + + view.context.strExpr = 'foo bar'; + view.detectChanges(); + expect(view.rootNodes[0].className).toEqual('ng-binding foo bar'); + + view.context.strExpr = 'baz'; + view.detectChanges(); + expect(view.rootNodes[0].className).toEqual('ng-binding baz'); + + async.done(); + }); + })); + }); + + it('should remove active classes when expression evaluates to null', inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => { - var template = '
'; + var template = '
'; tb.createView(TestComponent, {html: template}) .then((view) => { view.detectChanges(); expect(view.rootNodes[0].className).toEqual('ng-binding foo'); - StringMapWrapper.set(view.context.expr, 'bar', true); - view.detectChanges(); - expect(view.rootNodes[0].className).toEqual('ng-binding foo bar'); - - StringMapWrapper.set(view.context.expr, 'baz', true); + view.context.objExpr = null; view.detectChanges(); - expect(view.rootNodes[0].className).toEqual('ng-binding foo bar baz'); + expect(view.rootNodes[0].className).toEqual('ng-binding'); - StringMapWrapper.delete(view.context.expr, 'bar'); + view.context.objExpr = {'foo': false, 'bar': true}; view.detectChanges(); - expect(view.rootNodes[0].className).toEqual('ng-binding foo baz'); + expect(view.rootNodes[0].className).toEqual('ng-binding bar'); async.done(); }); })); - it('should retain existing classes when expression evaluates to null', + it('should have no effect when activated by a static class attribute', inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => { - var template = '
'; + var template = '
'; tb.createView(TestComponent, {html: template}) .then((view) => { view.detectChanges(); - expect(view.rootNodes[0].className).toEqual('ng-binding foo'); - - view.context.expr = null; - view.detectChanges(); - expect(view.rootNodes[0].className).toEqual('ng-binding foo'); - - view.context.expr = {'foo': false, 'bar': true}; - view.detectChanges(); - expect(view.rootNodes[0].className).toEqual('ng-binding bar'); - + // TODO(pk): in CJS className isn't initialized properly if we don't mutate classes + expect(ListWrapper.join(DOM.classList(view.rootNodes[0]), ' ')) + .toEqual('init foo ng-binding'); async.done(); }); })); it('should co-operate with the class attribute', inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => { - var template = '
'; + var template = '
'; tb.createView(TestComponent, {html: template}) .then((view) => { - StringMapWrapper.set(view.context.expr, 'bar', true); + StringMapWrapper.set(view.context.objExpr, 'bar', true); view.detectChanges(); expect(view.rootNodes[0].className).toEqual('init foo ng-binding bar'); - StringMapWrapper.set(view.context.expr, 'foo', false); + StringMapWrapper.set(view.context.objExpr, 'foo', false); view.detectChanges(); expect(view.rootNodes[0].className).toEqual('init ng-binding bar'); @@ -120,18 +253,18 @@ export function main() { it('should co-operate with the class attribute and class.name binding', inject([TestBed, AsyncTestCompleter], (tb: TestBed, async) => { - var template = '
'; + var template = '
'; tb.createView(TestComponent, {html: template}) .then((view) => { view.detectChanges(); expect(view.rootNodes[0].className).toEqual('init foo ng-binding baz'); - StringMapWrapper.set(view.context.expr, 'bar', true); + StringMapWrapper.set(view.context.objExpr, 'bar', true); view.detectChanges(); expect(view.rootNodes[0].className).toEqual('init foo ng-binding baz bar'); - StringMapWrapper.set(view.context.expr, 'foo', false); + StringMapWrapper.set(view.context.objExpr, 'foo', false); view.detectChanges(); expect(view.rootNodes[0].className).toEqual('init ng-binding baz bar'); @@ -148,10 +281,8 @@ export function main() { @Component({selector: 'test-cmp'}) @View({directives: [CSSClass]}) class TestComponent { - condition: boolean; - expr; - constructor() { - this.condition = true; - this.expr = {'foo': true, 'bar': false}; - } + condition: boolean = true; + arrExpr: List = ['foo']; + objExpr = {'foo': true, 'bar': false}; + strExpr = 'foo'; }