diff --git a/modules/directives/src/ng_switch.js b/modules/directives/src/ng_switch.js new file mode 100644 index 0000000000..50a122f6f0 --- /dev/null +++ b/modules/directives/src/ng_switch.js @@ -0,0 +1,190 @@ +import {Decorator, Template} from 'core/annotations/annotations'; +import {ViewPort} from 'core/compiler/viewport'; +import {NgElement} from 'core/dom/element'; +import {DOM} from 'facade/dom'; +import {isPresent, isBlank} from 'facade/lang'; +import {ListWrapper, List, MapWrapper, Map} from 'facade/collection'; +import {Parent} from 'core/annotations/visibility'; + +/** + * The `ngSwitch` directive is used to conditionally swap DOM structure on your template based on a + * scope expression. + * Elements within `ngSwitch` but without `ngSwitchWhen` or `ngSwitchDefault` directives will be + * preserved at the location as specified in the template. + * + * `ngSwitch` simply chooses nested elements and makes them visible based on which element matches + * the value obtained from the evaluated expression. In other words, you define a container element + * (where you place the directive), place an expression on the **`[ng-switch]="..."` attribute**), + * define any inner elements inside of the directive and place a `[ng-switch-when]` attribute per + * element. + * The when attribute is used to inform ngSwitch which element to display when the expression is + * evaluated. If a matching expression is not found via a when attribute then an element with the + * default attribute is displayed. + * + * Example: + * + * ``` + * + * + * + * + * + * ``` + */ +@Decorator({ + selector: '[ng-switch]', + bind: { + 'ng-switch': 'value' + } +}) +export class NgSwitch { + _switchValue: any; + _useDefault: boolean; + _valueViewPorts: Map; + _activeViewPorts: List; + + constructor() { + this._valueViewPorts = MapWrapper.create(); + this._activeViewPorts = ListWrapper.create(); + this._useDefault = false; + } + + set value(value) { + // Remove the currently active viewports + this._removeAllActiveViewPorts(); + + // Add the viewports matching the value (with a fallback to default) + this._useDefault = false; + var viewPorts = MapWrapper.get(this._valueViewPorts, value); + if (isBlank(viewPorts)) { + this._useDefault = true; + viewPorts = MapWrapper.get(this._valueViewPorts, _whenDefault); + } + this._activateViewPorts(viewPorts); + + this._switchValue = value; + } + + _onWhenValueChanged(oldWhen, newWhen, viewPort: ViewPort) { + this._deregisterViewPort(oldWhen, viewPort); + this._registerViewPort(newWhen, viewPort); + + if (oldWhen === this._switchValue) { + viewPort.remove(); + ListWrapper.remove(this._activeViewPorts, viewPort); + } else if (newWhen === this._switchValue) { + if (this._useDefault) { + this._useDefault = false; + this._removeAllActiveViewPorts(); + } + viewPort.create(); + ListWrapper.push(this._activeViewPorts, viewPort); + } + + // Switch to default when there is no more active viewports + if (this._activeViewPorts.length === 0 && !this._useDefault) { + this._useDefault = true; + this._activateViewPorts(MapWrapper.get(this._valueViewPorts, _whenDefault)); + } + } + + _removeAllActiveViewPorts() { + var activeViewPorts = this._activeViewPorts; + for (var i = 0; i < activeViewPorts.length; i++) { + activeViewPorts[i].remove(); + } + this._activeViewPorts = ListWrapper.create(); + } + + _activateViewPorts(viewPorts) { + // TODO(vicb): assert(this._activeViewPorts.length === 0); + if (isPresent(viewPorts)) { + for (var i = 0; i < viewPorts.length; i++) { + viewPorts[i].create(); + } + this._activeViewPorts = viewPorts; + } + } + + _registerViewPort(value, viewPort: ViewPort) { + var viewPorts = MapWrapper.get(this._valueViewPorts, value); + if (isBlank(viewPorts)) { + viewPorts = ListWrapper.create(); + MapWrapper.set(this._valueViewPorts, value, viewPorts); + } + ListWrapper.push(viewPorts, viewPort); + } + + _deregisterViewPort(value, viewPort: ViewPort) { + // `_whenDefault` is used a marker for non-registered whens + if (value == _whenDefault) return; + var viewPorts = MapWrapper.get(this._valueViewPorts, value); + if (viewPorts.length == 1) { + MapWrapper.delete(this._valueViewPorts, value); + } else { + ListWrapper.remove(viewPorts, viewPort); + } + } +} + +/** + * Defines a case statement as an expression. + * + * If multiple `ngSwitchWhen` match the `ngSwitch` value, all of them are displayed. + * + * Example: + * + * ``` + * // match against a context variable + * + * + * // match against a constant string + * + * ``` + */ +@Template({ + selector: '[ng-switch-when]', + bind: { + 'ng-switch-when' : 'when' + } +}) +export class NgSwitchWhen { + _value: any; + _ngSwitch: NgSwitch; + _viewPort: ViewPort; + + constructor(el: NgElement, viewPort: ViewPort, @Parent() ngSwitch: NgSwitch) { + // `_whenDefault` is used as a marker for a not yet initialized value + this._value = _whenDefault; + this._ngSwitch = ngSwitch; + this._viewPort = viewPort; + } + + set when(value) { + this._ngSwitch._onWhenValueChanged(this._value, value, this._viewPort); + this._value = value; + } +} + + +/** + * Defines a default case statement. + * + * Default case statements are displayed when no `NgSwitchWhen` match the `ngSwitch` value. + * + * Example: + * + * ``` + * + * ``` + */ +@Template({ + selector: '[ng-switch-default]' +}) +export class NgSwitchDefault { + constructor(viewPort: ViewPort, @Parent() ngSwitch: NgSwitch) { + ngSwitch._registerViewPort(_whenDefault, viewPort); + } +} + +var _whenDefault = new Object(); diff --git a/modules/directives/test/ng_repeat_spec.js b/modules/directives/test/ng_repeat_spec.js index ba90c9906a..adb939c4e0 100644 --- a/modules/directives/test/ng_repeat_spec.js +++ b/modules/directives/test/ng_repeat_spec.js @@ -3,7 +3,7 @@ import {describe, xit, it, expect, beforeEach, ddescribe, iit, el} from 'test_li import {DOM} from 'facade/dom'; import {Injector} from 'di/di'; -import {Lexer, Parser, ChangeDetector} from 'change_detection/change_detection'; +import {Lexer, Parser} from 'change_detection/change_detection'; import {Compiler, CompilerCache} from 'core/compiler/compiler'; import {OnChange} from 'core/compiler/interfaces'; diff --git a/modules/directives/test/ng_switch_spec.js b/modules/directives/test/ng_switch_spec.js new file mode 100644 index 0000000000..f7c1fdbe71 --- /dev/null +++ b/modules/directives/test/ng_switch_spec.js @@ -0,0 +1,161 @@ +import {describe, xit, it, expect, beforeEach, ddescribe, iit, el} from 'test_lib/test_lib'; +import {DOM} from 'facade/dom'; +import {Injector} from 'di/di'; +import {Lexer, Parser} from 'change_detection/change_detection'; +import {Compiler, CompilerCache} from 'core/compiler/compiler'; +import {DirectiveMetadataReader} from 'core/compiler/directive_metadata_reader'; +import {Component} from 'core/annotations/annotations'; +import {TemplateConfig} from 'core/annotations/template_config'; +import {NgSwitch, NgSwitchWhen, NgSwitchDefault} from 'directives/ng_switch'; + +export function main() { + describe('ng-switch', () => { + var view, cd, compiler, component; + beforeEach(() => { + compiler = new Compiler(null, new DirectiveMetadataReader(), new Parser(new Lexer()), new CompilerCache()); + }); + + function createView(pv) { + component = new TestComponent(); + view = pv.instantiate(null); + view.hydrate(new Injector([]), null, component); + cd = view.changeDetector; + } + + function compileWithTemplate(template) { + return compiler.compile(TestComponent, el(template)); + } + + describe('switch value changes', () => { + it('should switch amongst when values', (done) => { + var template = '
' + + '
'; + compileWithTemplate(template).then((pv) => { + createView(pv); + cd.detectChanges(); + expect(DOM.getText(view.nodes[0])).toEqual(''); + + component.switchValue = 'a'; + cd.detectChanges(); + expect(DOM.getText(view.nodes[0])).toEqual('when a'); + + component.switchValue = 'b'; + cd.detectChanges(); + expect(DOM.getText(view.nodes[0])).toEqual('when b'); + + done(); + }); + }); + + it('should switch amongst when values with fallback to default', (done) => { + var template = '
' + + '
'; + compileWithTemplate(template).then((pv) => { + createView(pv); + cd.detectChanges(); + expect(DOM.getText(view.nodes[0])).toEqual('when default'); + + component.switchValue = 'a'; + cd.detectChanges(); + expect(DOM.getText(view.nodes[0])).toEqual('when a'); + + component.switchValue = 'b'; + cd.detectChanges(); + expect(DOM.getText(view.nodes[0])).toEqual('when default'); + + done(); + }); + }); + + it('should support multiple whens with the same value', (done) => { + var template = '
' + + '
'; + compileWithTemplate(template).then((pv) => { + createView(pv); + cd.detectChanges(); + expect(DOM.getText(view.nodes[0])).toEqual('when default1;when default2;'); + + component.switchValue = 'a'; + cd.detectChanges(); + expect(DOM.getText(view.nodes[0])).toEqual('when a1;when a2;'); + + component.switchValue = 'b'; + cd.detectChanges(); + expect(DOM.getText(view.nodes[0])).toEqual('when b1;when b2;'); + + done(); + }); + }); + }); + + describe('when values changes', () => { + it('should switch amongst when values', (done) => { + var template = '
' + + '
'; + compileWithTemplate(template).then((pv) => { + createView(pv); + + component.when1 = 'a'; + component.when2 = 'b'; + component.switchValue = 'a'; + cd.detectChanges(); + expect(DOM.getText(view.nodes[0])).toEqual('when 1;'); + + component.switchValue = 'b'; + cd.detectChanges(); + expect(DOM.getText(view.nodes[0])).toEqual('when 2;'); + + component.switchValue = 'c'; + cd.detectChanges(); + expect(DOM.getText(view.nodes[0])).toEqual('when default;'); + + component.when1 = 'c'; + cd.detectChanges(); + expect(DOM.getText(view.nodes[0])).toEqual('when 1;'); + + component.when1 = 'd'; + cd.detectChanges(); + expect(DOM.getText(view.nodes[0])).toEqual('when default;'); + + done(); + }); + }); + }); + }); +} + +@Component({ + selector: 'test-cmp', + template: new TemplateConfig({ + inline: '', // each test swaps with a custom template. + directives: [NgSwitch, NgSwitchWhen, NgSwitchDefault] + }) +}) +class TestComponent { + switchValue: any; + when1: any; + when2: any; + + constructor() { + this.switchValue = null; + this.when1 = null; + this.when2 = null; + } +}