X Tutup
Skip to content

Commit df1f78e

Browse files
karaalexeagle
authored andcommitted
feat(i18n): add ngPlural directive
1 parent 43bb31c commit df1f78e

File tree

6 files changed

+315
-3
lines changed

6 files changed

+315
-3
lines changed

modules/angular2/src/common/directives.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ export {NgFor} from './directives/ng_for';
88
export {NgIf} from './directives/ng_if';
99
export {NgStyle} from './directives/ng_style';
1010
export {NgSwitch, NgSwitchWhen, NgSwitchDefault} from './directives/ng_switch';
11+
export {NgPlural, NgPluralCase, NgLocalization} from './directives/ng_plural';
1112
export * from './directives/observable_list_diff';
12-
export {CORE_DIRECTIVES} from './directives/core_directives';
13+
export {CORE_DIRECTIVES} from './directives/core_directives';

modules/angular2/src/common/directives/core_directives.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {NgFor} from './ng_for';
44
import {NgIf} from './ng_if';
55
import {NgStyle} from './ng_style';
66
import {NgSwitch, NgSwitchWhen, NgSwitchDefault} from './ng_switch';
7+
import {NgPlural, NgPluralCase} from './ng_plural';
78

89
/**
910
* 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';
4546
* }
4647
* ```
4748
*/
48-
export const CORE_DIRECTIVES: Type[] =
49-
CONST_EXPR([NgClass, NgFor, NgIf, NgStyle, NgSwitch, NgSwitchWhen, NgSwitchDefault]);
49+
export const CORE_DIRECTIVES: Type[] = CONST_EXPR([
50+
NgClass,
51+
NgFor,
52+
NgIf,
53+
NgStyle,
54+
NgSwitch,
55+
NgSwitchWhen,
56+
NgSwitchDefault,
57+
NgPlural,
58+
NgPluralCase
59+
]);
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import {
2+
Directive,
3+
ViewContainerRef,
4+
TemplateRef,
5+
ContentChildren,
6+
QueryList,
7+
Attribute,
8+
AfterContentInit,
9+
Input
10+
} from 'angular2/core';
11+
import {isPresent, NumberWrapper} from 'angular2/src/facade/lang';
12+
import {Map} from 'angular2/src/facade/collection';
13+
import {SwitchView} from './ng_switch';
14+
15+
const _CATEGORY_DEFAULT = 'other';
16+
17+
export abstract class NgLocalization { abstract getPluralCategory(value: any): string; }
18+
19+
/**
20+
* `ngPlural` is an i18n directive that displays DOM sub-trees that match the switch expression
21+
* value, or failing that, DOM sub-trees that match the switch expression's pluralization category.
22+
*
23+
* To use this directive, you must provide an extension of `NgLocalization` that maps values to
24+
* category names. You then define a container element that sets the `[ngPlural]` attribute to a
25+
* switch expression.
26+
* - Inner elements defined with an `[ngPluralCase]` attribute will display based on their
27+
* expression.
28+
* - If `[ngPluralCase]` is set to a value starting with `=`, it will only display if the value
29+
* matches the switch expression exactly.
30+
* - Otherwise, the view will be treated as a "category match", and will only display if exact
31+
* value matches aren't found and the value maps to its category using the `getPluralCategory`
32+
* function provided.
33+
*
34+
* If no matching views are found for a switch expression, inner elements marked
35+
* `[ngPluralCase]="other"` will be displayed.
36+
*
37+
* ```typescript
38+
* class MyLocalization extends NgLocalization {
39+
* getPluralCategory(value: any) {
40+
* if(value < 5) {
41+
* return 'few';
42+
* }
43+
* }
44+
* }
45+
*
46+
* @Component({
47+
* selector: 'app',
48+
* providers: [provide(NgLocalization, {useClass: MyLocalization})]
49+
* })
50+
* @View({
51+
* template: `
52+
* <p>Value = {{value}}</p>
53+
* <button (click)="inc()">Increment</button>
54+
*
55+
* <div [ngPlural]="value">
56+
* <template ngPluralCase="=0">there is nothing</template>
57+
* <template ngPluralCase="=1">there is one</template>
58+
* <template ngPluralCase="few">there are a few</template>
59+
* <template ngPluralCase="other">there is some number</template>
60+
* </div>
61+
* `,
62+
* directives: [NgPlural, NgPluralCase]
63+
* })
64+
* export class App {
65+
* value = 'init';
66+
*
67+
* inc() {
68+
* this.value = this.value === 'init' ? 0 : this.value + 1;
69+
* }
70+
* }
71+
*
72+
* ```
73+
*/
74+
75+
@Directive({selector: '[ngPluralCase]'})
76+
export class NgPluralCase {
77+
_view: SwitchView;
78+
constructor(@Attribute('ngPluralCase') public value: string, template: TemplateRef,
79+
viewContainer: ViewContainerRef) {
80+
this._view = new SwitchView(viewContainer, template);
81+
}
82+
}
83+
84+
85+
@Directive({selector: '[ngPlural]'})
86+
export class NgPlural implements AfterContentInit {
87+
private _switchValue: number;
88+
private _activeView: SwitchView;
89+
private _caseViews = new Map<any, SwitchView>();
90+
@ContentChildren(NgPluralCase) cases: QueryList<NgPluralCase> = null;
91+
92+
constructor(private _localization: NgLocalization) {}
93+
94+
@Input()
95+
set ngPlural(value: number) {
96+
this._switchValue = value;
97+
this._updateView();
98+
}
99+
100+
ngAfterContentInit() {
101+
this.cases.forEach((pluralCase: NgPluralCase): void => {
102+
this._caseViews.set(this._formatValue(pluralCase), pluralCase._view);
103+
});
104+
this._updateView();
105+
}
106+
107+
/** @internal */
108+
_updateView(): void {
109+
this._clearViews();
110+
111+
var view: SwitchView = this._caseViews.get(this._switchValue);
112+
if (!isPresent(view)) view = this._getCategoryView(this._switchValue);
113+
114+
this._activateView(view);
115+
}
116+
117+
/** @internal */
118+
_clearViews() {
119+
if (isPresent(this._activeView)) this._activeView.destroy();
120+
}
121+
122+
/** @internal */
123+
_activateView(view: SwitchView) {
124+
if (!isPresent(view)) return;
125+
this._activeView = view;
126+
this._activeView.create();
127+
}
128+
129+
/** @internal */
130+
_getCategoryView(value: number): SwitchView {
131+
var category: string = this._localization.getPluralCategory(value);
132+
var categoryView: SwitchView = this._caseViews.get(category);
133+
return isPresent(categoryView) ? categoryView : this._caseViews.get(_CATEGORY_DEFAULT);
134+
}
135+
136+
/** @internal */
137+
_isValueView(pluralCase: NgPluralCase): boolean { return pluralCase.value[0] === "="; }
138+
139+
/** @internal */
140+
_formatValue(pluralCase: NgPluralCase): any {
141+
return this._isValueView(pluralCase) ? this._stripValue(pluralCase.value) : pluralCase.value;
142+
}
143+
144+
/** @internal */
145+
_stripValue(value: string): number { return NumberWrapper.parseInt(value.substring(1), 10); }
146+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import {
2+
AsyncTestCompleter,
3+
TestComponentBuilder,
4+
beforeEachProviders,
5+
beforeEach,
6+
ddescribe,
7+
describe,
8+
el,
9+
expect,
10+
iit,
11+
inject,
12+
it,
13+
xit,
14+
} from 'angular2/testing_internal';
15+
16+
import {Component, View, Injectable, provide} from 'angular2/core';
17+
import {NgPlural, NgPluralCase, NgLocalization} from 'angular2/common';
18+
19+
export function main() {
20+
describe('switch', () => {
21+
beforeEachProviders(() => [provide(NgLocalization, {useClass: TestLocalizationMap})]);
22+
23+
it('should display the template according to the exact value',
24+
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
25+
var template = '<div>' +
26+
'<ul [ngPlural]="switchValue">' +
27+
'<template ngPluralCase="=0"><li>you have no messages.</li></template>' +
28+
'<template ngPluralCase="=1"><li>you have one message.</li></template>' +
29+
'</ul></div>';
30+
31+
tcb.overrideTemplate(TestComponent, template)
32+
.createAsync(TestComponent)
33+
.then((fixture) => {
34+
fixture.debugElement.componentInstance.switchValue = 0;
35+
fixture.detectChanges();
36+
expect(fixture.debugElement.nativeElement).toHaveText('you have no messages.');
37+
38+
fixture.debugElement.componentInstance.switchValue = 1;
39+
fixture.detectChanges();
40+
expect(fixture.debugElement.nativeElement).toHaveText('you have one message.');
41+
42+
async.done();
43+
});
44+
}));
45+
46+
it('should display the template according to the category',
47+
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
48+
var template =
49+
'<div>' +
50+
'<ul [ngPlural]="switchValue">' +
51+
'<template ngPluralCase="few"><li>you have a few messages.</li></template>' +
52+
'<template ngPluralCase="many"><li>you have many messages.</li></template>' +
53+
'</ul></div>';
54+
55+
tcb.overrideTemplate(TestComponent, template)
56+
.createAsync(TestComponent)
57+
.then((fixture) => {
58+
fixture.debugElement.componentInstance.switchValue = 2;
59+
fixture.detectChanges();
60+
expect(fixture.debugElement.nativeElement).toHaveText('you have a few messages.');
61+
62+
fixture.debugElement.componentInstance.switchValue = 8;
63+
fixture.detectChanges();
64+
expect(fixture.debugElement.nativeElement).toHaveText('you have many messages.');
65+
66+
async.done();
67+
});
68+
}));
69+
70+
it('should default to other when no matches are found',
71+
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
72+
var template =
73+
'<div>' +
74+
'<ul [ngPlural]="switchValue">' +
75+
'<template ngPluralCase="few"><li>you have a few messages.</li></template>' +
76+
'<template ngPluralCase="other"><li>default message.</li></template>' +
77+
'</ul></div>';
78+
79+
tcb.overrideTemplate(TestComponent, template)
80+
.createAsync(TestComponent)
81+
.then((fixture) => {
82+
fixture.debugElement.componentInstance.switchValue = 100;
83+
fixture.detectChanges();
84+
expect(fixture.debugElement.nativeElement).toHaveText('default message.');
85+
86+
async.done();
87+
});
88+
}));
89+
90+
it('should prioritize value matches over category matches',
91+
inject([TestComponentBuilder, AsyncTestCompleter], (tcb: TestComponentBuilder, async) => {
92+
var template =
93+
'<div>' +
94+
'<ul [ngPlural]="switchValue">' +
95+
'<template ngPluralCase="few"><li>you have a few messages.</li></template>' +
96+
'<template ngPluralCase="=2">you have two messages.</template>' +
97+
'</ul></div>';
98+
99+
tcb.overrideTemplate(TestComponent, template)
100+
.createAsync(TestComponent)
101+
.then((fixture) => {
102+
fixture.debugElement.componentInstance.switchValue = 2;
103+
fixture.detectChanges();
104+
expect(fixture.debugElement.nativeElement).toHaveText('you have two messages.');
105+
106+
fixture.debugElement.componentInstance.switchValue = 3;
107+
fixture.detectChanges();
108+
expect(fixture.debugElement.nativeElement).toHaveText('you have a few messages.');
109+
110+
async.done();
111+
});
112+
}));
113+
});
114+
}
115+
116+
117+
@Injectable()
118+
export class TestLocalizationMap extends NgLocalization {
119+
getPluralCategory(value: number): string {
120+
if (value > 1 && value < 4) {
121+
return 'few';
122+
} else if (value >= 4 && value < 10) {
123+
return 'many';
124+
} else {
125+
return 'other';
126+
}
127+
}
128+
}
129+
130+
131+
@Component({selector: 'test-cmp'})
132+
@View({directives: [NgPlural, NgPluralCase]})
133+
class TestComponent {
134+
switchValue: number;
135+
136+
constructor() { this.switchValue = null; }
137+
}

modules/angular2/test/public_api_spec.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,7 @@ var NG_COMMON = [
381381
'NgFormModel.value',
382382
'NgIf',
383383
'NgIf.ngIf=',
384+
'NgLocalization',
384385
'NgModel',
385386
'NgModel.asyncValidator',
386387
'NgModel.control',
@@ -405,6 +406,14 @@ var NG_COMMON = [
405406
'NgModel.viewModel',
406407
'NgModel.viewModel=',
407408
'NgModel.viewToModelUpdate()',
409+
'NgPlural',
410+
'NgPlural.cases',
411+
'NgPlural.cases=',
412+
'NgPlural.ngAfterContentInit()',
413+
'NgPlural.ngPlural=',
414+
'NgPluralCase',
415+
'NgPluralCase.value',
416+
'NgPluralCase.value=',
408417
'NgSelectOption',
409418
'NgStyle',
410419
'NgStyle.ngDoCheck()',

tools/public_api_guard/public_api_spec.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -760,6 +760,8 @@ const COMMON = [
760760
'NgIf',
761761
'NgIf.constructor(_viewContainer:ViewContainerRef, _templateRef:TemplateRef)',
762762
'NgIf.ngIf=(newCondition:any)',
763+
'NgLocalization',
764+
'NgLocalization.getPluralCategory(value:any):string',
763765
'NgModel',
764766
'NgModel.asyncValidator:AsyncValidatorFn',
765767
'NgModel.constructor(_validators:any[], _asyncValidators:any[], valueAccessors:ControlValueAccessor[])',
@@ -771,6 +773,13 @@ const COMMON = [
771773
'NgModel.validator:ValidatorFn',
772774
'NgModel.viewModel:any',
773775
'NgModel.viewToModelUpdate(newValue:any):void',
776+
'NgPlural',
777+
'NgPlural.cases:QueryList<NgPluralCase>',
778+
'NgPlural.constructor(_localization:NgLocalization)',
779+
'NgPlural.ngAfterContentInit():any',
780+
'NgPluralCase.constructor(value:string, template:TemplateRef, viewContainer:ViewContainerRef)',
781+
'NgPlural.ngPlural=(value:number)',
782+
'NgPluralCase',
774783
'NgSelectOption',
775784
'NgStyle',
776785
'NgStyle.constructor(_differs:KeyValueDiffers, _ngEl:ElementRef, _renderer:Renderer)',

0 commit comments

Comments
 (0)
X Tutup