feat(i18n): add ngPlural directive
This commit is contained in:
parent
43bb31c6c6
commit
df1f78e302
|
@ -8,5 +8,6 @@ export {NgFor} from './directives/ng_for';
|
||||||
export {NgIf} from './directives/ng_if';
|
export {NgIf} from './directives/ng_if';
|
||||||
export {NgStyle} from './directives/ng_style';
|
export {NgStyle} from './directives/ng_style';
|
||||||
export {NgSwitch, NgSwitchWhen, NgSwitchDefault} from './directives/ng_switch';
|
export {NgSwitch, NgSwitchWhen, NgSwitchDefault} from './directives/ng_switch';
|
||||||
|
export {NgPlural, NgPluralCase, NgLocalization} from './directives/ng_plural';
|
||||||
export * from './directives/observable_list_diff';
|
export * from './directives/observable_list_diff';
|
||||||
export {CORE_DIRECTIVES} from './directives/core_directives';
|
export {CORE_DIRECTIVES} from './directives/core_directives';
|
|
@ -4,6 +4,7 @@ import {NgFor} from './ng_for';
|
||||||
import {NgIf} from './ng_if';
|
import {NgIf} from './ng_if';
|
||||||
import {NgStyle} from './ng_style';
|
import {NgStyle} from './ng_style';
|
||||||
import {NgSwitch, NgSwitchWhen, NgSwitchDefault} from './ng_switch';
|
import {NgSwitch, NgSwitchWhen, NgSwitchDefault} from './ng_switch';
|
||||||
|
import {NgPlural, NgPluralCase} from './ng_plural';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A collection of Angular core directives that are likely to be used in each and every Angular
|
* A collection of Angular core directives that are likely to be used in each and every Angular
|
||||||
|
@ -45,5 +46,14 @@ import {NgSwitch, NgSwitchWhen, NgSwitchDefault} from './ng_switch';
|
||||||
* }
|
* }
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export const CORE_DIRECTIVES: Type[] =
|
export const CORE_DIRECTIVES: Type[] = CONST_EXPR([
|
||||||
CONST_EXPR([NgClass, NgFor, NgIf, NgStyle, NgSwitch, NgSwitchWhen, NgSwitchDefault]);
|
NgClass,
|
||||||
|
NgFor,
|
||||||
|
NgIf,
|
||||||
|
NgStyle,
|
||||||
|
NgSwitch,
|
||||||
|
NgSwitchWhen,
|
||||||
|
NgSwitchDefault,
|
||||||
|
NgPlural,
|
||||||
|
NgPluralCase
|
||||||
|
]);
|
||||||
|
|
|
@ -0,0 +1,146 @@
|
||||||
|
import {
|
||||||
|
Directive,
|
||||||
|
ViewContainerRef,
|
||||||
|
TemplateRef,
|
||||||
|
ContentChildren,
|
||||||
|
QueryList,
|
||||||
|
Attribute,
|
||||||
|
AfterContentInit,
|
||||||
|
Input
|
||||||
|
} from 'angular2/core';
|
||||||
|
import {isPresent, NumberWrapper} from 'angular2/src/facade/lang';
|
||||||
|
import {Map} from 'angular2/src/facade/collection';
|
||||||
|
import {SwitchView} from './ng_switch';
|
||||||
|
|
||||||
|
const _CATEGORY_DEFAULT = 'other';
|
||||||
|
|
||||||
|
export abstract class NgLocalization { abstract getPluralCategory(value: any): string; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `ngPlural` is an i18n directive that displays DOM sub-trees that match the switch expression
|
||||||
|
* value, or failing that, DOM sub-trees that match the switch expression's pluralization category.
|
||||||
|
*
|
||||||
|
* To use this directive, you must provide an extension of `NgLocalization` that maps values to
|
||||||
|
* category names. You then define a container element that sets the `[ngPlural]` attribute to a
|
||||||
|
* switch expression.
|
||||||
|
* - Inner elements defined with an `[ngPluralCase]` attribute will display based on their
|
||||||
|
* expression.
|
||||||
|
* - If `[ngPluralCase]` is set to a value starting with `=`, it will only display if the value
|
||||||
|
* matches the switch expression exactly.
|
||||||
|
* - Otherwise, the view will be treated as a "category match", and will only display if exact
|
||||||
|
* value matches aren't found and the value maps to its category using the `getPluralCategory`
|
||||||
|
* function provided.
|
||||||
|
*
|
||||||
|
* If no matching views are found for a switch expression, inner elements marked
|
||||||
|
* `[ngPluralCase]="other"` will be displayed.
|
||||||
|
*
|
||||||
|
* ```typescript
|
||||||
|
* class MyLocalization extends NgLocalization {
|
||||||
|
* getPluralCategory(value: any) {
|
||||||
|
* if(value < 5) {
|
||||||
|
* return 'few';
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* @Component({
|
||||||
|
* selector: 'app',
|
||||||
|
* providers: [provide(NgLocalization, {useClass: MyLocalization})]
|
||||||
|
* })
|
||||||
|
* @View({
|
||||||
|
* template: `
|
||||||
|
* <p>Value = {{value}}</p>
|
||||||
|
* <button (click)="inc()">Increment</button>
|
||||||
|
*
|
||||||
|
* <div [ngPlural]="value">
|
||||||
|
* <template ngPluralCase="=0">there is nothing</template>
|
||||||
|
* <template ngPluralCase="=1">there is one</template>
|
||||||
|
* <template ngPluralCase="few">there are a few</template>
|
||||||
|
* <template ngPluralCase="other">there is some number</template>
|
||||||
|
* </div>
|
||||||
|
* `,
|
||||||
|
* directives: [NgPlural, NgPluralCase]
|
||||||
|
* })
|
||||||
|
* export class App {
|
||||||
|
* value = 'init';
|
||||||
|
*
|
||||||
|
* inc() {
|
||||||
|
* this.value = this.value === 'init' ? 0 : this.value + 1;
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Directive({selector: '[ngPluralCase]'})
|
||||||
|
export class NgPluralCase {
|
||||||
|
_view: SwitchView;
|
||||||
|
constructor(@Attribute('ngPluralCase') public value: string, template: TemplateRef,
|
||||||
|
viewContainer: ViewContainerRef) {
|
||||||
|
this._view = new SwitchView(viewContainer, template);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Directive({selector: '[ngPlural]'})
|
||||||
|
export class NgPlural implements AfterContentInit {
|
||||||
|
private _switchValue: number;
|
||||||
|
private _activeView: SwitchView;
|
||||||
|
private _caseViews = new Map<any, SwitchView>();
|
||||||
|
@ContentChildren(NgPluralCase) cases: QueryList<NgPluralCase> = null;
|
||||||
|
|
||||||
|
constructor(private _localization: NgLocalization) {}
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set ngPlural(value: number) {
|
||||||
|
this._switchValue = value;
|
||||||
|
this._updateView();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterContentInit() {
|
||||||
|
this.cases.forEach((pluralCase: NgPluralCase): void => {
|
||||||
|
this._caseViews.set(this._formatValue(pluralCase), pluralCase._view);
|
||||||
|
});
|
||||||
|
this._updateView();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
_updateView(): void {
|
||||||
|
this._clearViews();
|
||||||
|
|
||||||
|
var view: SwitchView = this._caseViews.get(this._switchValue);
|
||||||
|
if (!isPresent(view)) view = this._getCategoryView(this._switchValue);
|
||||||
|
|
||||||
|
this._activateView(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
_clearViews() {
|
||||||
|
if (isPresent(this._activeView)) this._activeView.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
_activateView(view: SwitchView) {
|
||||||
|
if (!isPresent(view)) return;
|
||||||
|
this._activeView = view;
|
||||||
|
this._activeView.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
_getCategoryView(value: number): SwitchView {
|
||||||
|
var category: string = this._localization.getPluralCategory(value);
|
||||||
|
var categoryView: SwitchView = this._caseViews.get(category);
|
||||||
|
return isPresent(categoryView) ? categoryView : this._caseViews.get(_CATEGORY_DEFAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
_isValueView(pluralCase: NgPluralCase): boolean { return pluralCase.value[0] === "="; }
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
_formatValue(pluralCase: NgPluralCase): any {
|
||||||
|
return this._isValueView(pluralCase) ? this._stripValue(pluralCase.value) : pluralCase.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
_stripValue(value: string): number { return NumberWrapper.parseInt(value.substring(1), 10); }
|
||||||
|
}
|
|
@ -0,0 +1,137 @@
|
||||||
|
import {
|
||||||
|
AsyncTestCompleter,
|
||||||
|
TestComponentBuilder,
|
||||||
|
beforeEachProviders,
|
||||||
|
beforeEach,
|
||||||
|
ddescribe,
|
||||||
|
describe,
|
||||||
|
el,
|
||||||
|
expect,
|
||||||
|
iit,
|
||||||
|
inject,
|
||||||
|
it,
|
||||||
|
xit,
|
||||||
|
} from 'angular2/testing_internal';
|
||||||
|
|
||||||
|
import {Component, View, Injectable, provide} from 'angular2/core';
|
||||||
|
import {NgPlural, NgPluralCase, NgLocalization} from 'angular2/common';
|
||||||
|
|
||||||
|
export function main() {
|
||||||
|
describe('switch', () => {
|
||||||
|
beforeEachProviders(() => [provide(NgLocalization, {useClass: TestLocalizationMap})]);
|
||||||
|
|
||||||
|
it('should display the template according to the exact value',
|
||||||
|
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||||
|
var template = '<div>' +
|
||||||
|
'<ul [ngPlural]="switchValue">' +
|
||||||
|
'<template ngPluralCase="=0"><li>you have no messages.</li></template>' +
|
||||||
|
'<template ngPluralCase="=1"><li>you have one message.</li></template>' +
|
||||||
|
'</ul></div>';
|
||||||
|
|
||||||
|
tcb.overrideTemplate(TestComponent, template)
|
||||||
|
.createAsync(TestComponent)
|
||||||
|
.then((fixture) => {
|
||||||
|
fixture.debugElement.componentInstance.switchValue = 0;
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(fixture.debugElement.nativeElement).toHaveText('you have no messages.');
|
||||||
|
|
||||||
|
fixture.debugElement.componentInstance.switchValue = 1;
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(fixture.debugElement.nativeElement).toHaveText('you have one message.');
|
||||||
|
|
||||||
|
async.done();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should display the template according to the category',
|
||||||
|
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||||
|
var template =
|
||||||
|
'<div>' +
|
||||||
|
'<ul [ngPlural]="switchValue">' +
|
||||||
|
'<template ngPluralCase="few"><li>you have a few messages.</li></template>' +
|
||||||
|
'<template ngPluralCase="many"><li>you have many messages.</li></template>' +
|
||||||
|
'</ul></div>';
|
||||||
|
|
||||||
|
tcb.overrideTemplate(TestComponent, template)
|
||||||
|
.createAsync(TestComponent)
|
||||||
|
.then((fixture) => {
|
||||||
|
fixture.debugElement.componentInstance.switchValue = 2;
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(fixture.debugElement.nativeElement).toHaveText('you have a few messages.');
|
||||||
|
|
||||||
|
fixture.debugElement.componentInstance.switchValue = 8;
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(fixture.debugElement.nativeElement).toHaveText('you have many messages.');
|
||||||
|
|
||||||
|
async.done();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should default to other when no matches are found',
|
||||||
|
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||||
|
var template =
|
||||||
|
'<div>' +
|
||||||
|
'<ul [ngPlural]="switchValue">' +
|
||||||
|
'<template ngPluralCase="few"><li>you have a few messages.</li></template>' +
|
||||||
|
'<template ngPluralCase="other"><li>default message.</li></template>' +
|
||||||
|
'</ul></div>';
|
||||||
|
|
||||||
|
tcb.overrideTemplate(TestComponent, template)
|
||||||
|
.createAsync(TestComponent)
|
||||||
|
.then((fixture) => {
|
||||||
|
fixture.debugElement.componentInstance.switchValue = 100;
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(fixture.debugElement.nativeElement).toHaveText('default message.');
|
||||||
|
|
||||||
|
async.done();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should prioritize value matches over category matches',
|
||||||
|
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
|
||||||
|
var template =
|
||||||
|
'<div>' +
|
||||||
|
'<ul [ngPlural]="switchValue">' +
|
||||||
|
'<template ngPluralCase="few"><li>you have a few messages.</li></template>' +
|
||||||
|
'<template ngPluralCase="=2">you have two messages.</template>' +
|
||||||
|
'</ul></div>';
|
||||||
|
|
||||||
|
tcb.overrideTemplate(TestComponent, template)
|
||||||
|
.createAsync(TestComponent)
|
||||||
|
.then((fixture) => {
|
||||||
|
fixture.debugElement.componentInstance.switchValue = 2;
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(fixture.debugElement.nativeElement).toHaveText('you have two messages.');
|
||||||
|
|
||||||
|
fixture.debugElement.componentInstance.switchValue = 3;
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(fixture.debugElement.nativeElement).toHaveText('you have a few messages.');
|
||||||
|
|
||||||
|
async.done();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TestLocalizationMap extends NgLocalization {
|
||||||
|
getPluralCategory(value: number): string {
|
||||||
|
if (value > 1 && value < 4) {
|
||||||
|
return 'few';
|
||||||
|
} else if (value >= 4 && value < 10) {
|
||||||
|
return 'many';
|
||||||
|
} else {
|
||||||
|
return 'other';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Component({selector: 'test-cmp'})
|
||||||
|
@View({directives: [NgPlural, NgPluralCase]})
|
||||||
|
class TestComponent {
|
||||||
|
switchValue: number;
|
||||||
|
|
||||||
|
constructor() { this.switchValue = null; }
|
||||||
|
}
|
|
@ -381,6 +381,7 @@ var NG_COMMON = [
|
||||||
'NgFormModel.value',
|
'NgFormModel.value',
|
||||||
'NgIf',
|
'NgIf',
|
||||||
'NgIf.ngIf=',
|
'NgIf.ngIf=',
|
||||||
|
'NgLocalization',
|
||||||
'NgModel',
|
'NgModel',
|
||||||
'NgModel.asyncValidator',
|
'NgModel.asyncValidator',
|
||||||
'NgModel.control',
|
'NgModel.control',
|
||||||
|
@ -405,6 +406,14 @@ var NG_COMMON = [
|
||||||
'NgModel.viewModel',
|
'NgModel.viewModel',
|
||||||
'NgModel.viewModel=',
|
'NgModel.viewModel=',
|
||||||
'NgModel.viewToModelUpdate()',
|
'NgModel.viewToModelUpdate()',
|
||||||
|
'NgPlural',
|
||||||
|
'NgPlural.cases',
|
||||||
|
'NgPlural.cases=',
|
||||||
|
'NgPlural.ngAfterContentInit()',
|
||||||
|
'NgPlural.ngPlural=',
|
||||||
|
'NgPluralCase',
|
||||||
|
'NgPluralCase.value',
|
||||||
|
'NgPluralCase.value=',
|
||||||
'NgSelectOption',
|
'NgSelectOption',
|
||||||
'NgStyle',
|
'NgStyle',
|
||||||
'NgStyle.ngDoCheck()',
|
'NgStyle.ngDoCheck()',
|
||||||
|
|
|
@ -760,6 +760,8 @@ const COMMON = [
|
||||||
'NgIf',
|
'NgIf',
|
||||||
'NgIf.constructor(_viewContainer:ViewContainerRef, _templateRef:TemplateRef)',
|
'NgIf.constructor(_viewContainer:ViewContainerRef, _templateRef:TemplateRef)',
|
||||||
'NgIf.ngIf=(newCondition:any)',
|
'NgIf.ngIf=(newCondition:any)',
|
||||||
|
'NgLocalization',
|
||||||
|
'NgLocalization.getPluralCategory(value:any):string',
|
||||||
'NgModel',
|
'NgModel',
|
||||||
'NgModel.asyncValidator:AsyncValidatorFn',
|
'NgModel.asyncValidator:AsyncValidatorFn',
|
||||||
'NgModel.constructor(_validators:any[], _asyncValidators:any[], valueAccessors:ControlValueAccessor[])',
|
'NgModel.constructor(_validators:any[], _asyncValidators:any[], valueAccessors:ControlValueAccessor[])',
|
||||||
|
@ -771,6 +773,13 @@ const COMMON = [
|
||||||
'NgModel.validator:ValidatorFn',
|
'NgModel.validator:ValidatorFn',
|
||||||
'NgModel.viewModel:any',
|
'NgModel.viewModel:any',
|
||||||
'NgModel.viewToModelUpdate(newValue:any):void',
|
'NgModel.viewToModelUpdate(newValue:any):void',
|
||||||
|
'NgPlural',
|
||||||
|
'NgPlural.cases:QueryList<NgPluralCase>',
|
||||||
|
'NgPlural.constructor(_localization:NgLocalization)',
|
||||||
|
'NgPlural.ngAfterContentInit():any',
|
||||||
|
'NgPluralCase.constructor(value:string, template:TemplateRef, viewContainer:ViewContainerRef)',
|
||||||
|
'NgPlural.ngPlural=(value:number)',
|
||||||
|
'NgPluralCase',
|
||||||
'NgSelectOption',
|
'NgSelectOption',
|
||||||
'NgStyle',
|
'NgStyle',
|
||||||
'NgStyle.constructor(_differs:KeyValueDiffers, _ngEl:ElementRef, _renderer:Renderer)',
|
'NgStyle.constructor(_differs:KeyValueDiffers, _ngEl:ElementRef, _renderer:Renderer)',
|
||||||
|
|
Loading…
Reference in New Issue