From 51d951e37e7c4ec8690eae860bc682ebf940d511 Mon Sep 17 00:00:00 2001 From: Victor Date: Wed, 18 Dec 2013 19:26:59 +0100 Subject: [PATCH] feat(ng-pluralize): Implement the ng-pluralize directive --- lib/directive/module.dart | 3 + lib/directive/ng_pluralize.dart | 154 ++++++++++++++++++++ test/directive/ng_pluralize_spec.dart | 193 ++++++++++++++++++++++++++ 3 files changed, 350 insertions(+) create mode 100644 lib/directive/ng_pluralize.dart create mode 100644 test/directive/ng_pluralize_spec.dart diff --git a/lib/directive/module.dart b/lib/directive/module.dart index eb85f15ec..e187bb4e8 100644 --- a/lib/directive/module.dart +++ b/lib/directive/module.dart @@ -3,6 +3,7 @@ library angular.directive; import 'package:di/di.dart'; import 'dart:html' as dom; import 'dart:async' as async; +import 'package:intl/intl.dart'; import 'package:angular/core/module.dart'; import 'package:angular/core/parser/parser_library.dart'; import 'package:angular/core_dom/module.dart'; @@ -18,6 +19,7 @@ part 'ng_cloak.dart'; part 'ng_if.dart'; part 'ng_include.dart'; part 'ng_model.dart'; +part 'ng_pluralize.dart'; part 'ng_repeat.dart'; part 'ng_template.dart'; part 'ng_show_hide.dart'; @@ -42,6 +44,7 @@ class NgDirectiveModule extends Module { value(NgIfDirective, null); value(NgUnlessDirective, null); value(NgIncludeDirective, null); + value(NgPluralizeDirective, null); value(NgRepeatDirective, null); value(NgShalowRepeatDirective, null); value(NgShowDirective, null); diff --git a/lib/directive/ng_pluralize.dart b/lib/directive/ng_pluralize.dart new file mode 100644 index 000000000..b12712e32 --- /dev/null +++ b/lib/directive/ng_pluralize.dart @@ -0,0 +1,154 @@ +part of angular.directive; + +/** + * ## Overview + * `ngPluralize` is a directive that displays messages according to locale rules. + * + * You configure ngPluralize directive by specifying the mappings between plural + * categories and the strings to be displayed. + * + * ## Plural categories and explicit number rules + * The available plural categories are: + * * "zero", + * * "one", + * * "two", + * * "few", + * * "many", + * * "other". + * + * While a plural category may match many numbers, an explicit number rule can only match + * one number. For example, the explicit number rule for "3" matches the number 3. There + * are examples of plural categories and explicit number rules throughout the rest of this + * documentation. + * + * ## Configuring ngPluralize + * You configure ngPluralize by providing 2 attributes: `count` and `when`. + * You can also provide an optional attribute, `offset`. + * + * The value of the `count` attribute can be either a string or an expression; these are + * evaluated on the current scope for its bound value. + * + * The `when` attribute specifies the mappings between plural categories and the actual + * string to be displayed. The value of the attribute should be a JSON object. + * + * The following example shows how to configure ngPluralize: + * + * + * + * + * In the example, `"0: Nobody is viewing."` is an explicit number rule. If you did not + * specify this rule, 0 would be matched to the "other" category and "0 people are viewing" + * would be shown instead of "Nobody is viewing". You can specify an explicit number rule for + * other numbers, for example 12, so that instead of showing "12 people are viewing", you can + * show "a dozen people are viewing". + * + * You can use a set of closed braces (`{}`) as a placeholder for the number that you want substituted + * into pluralized strings. In the previous example, Angular will replace `{}` with + * `{{personCount}}`. The closed braces `{}` is a placeholder {{numberExpression}}. + * + * ## Configuring ngPluralize with offset + * The `offset` attribute allows further customization of pluralized text, which can result in + * a better user experience. For example, instead of the message "4 people are viewing this document", + * you might display "John, Kate and 2 others are viewing this document". + * The offset attribute allows you to offset a number by any desired value. + * Let's take a look at an example: + * + * + * + * + * Notice that we are still using two plural categories(one, other), but we added + * three explicit number rules 0, 1 and 2. + * When one person, perhaps John, views the document, "John is viewing" will be shown. + * When three people view the document, no explicit number rule is found, so + * an offset of 2 is taken off 3, and Angular uses 1 to decide the plural category. + * In this case, plural category 'one' is matched and "John, Marry and one other person are viewing" + * is shown. + * + * Note that when you specify offsets, you must provide explicit number rules for + * numbers from 0 up to and including the offset. If you use an offset of 3, for example, + * you must provide explicit number rules for 0, 1, 2 and 3. You must also provide plural strings for + * at least the "other" plural category. + */ + @NgDirective( + selector: 'ng-pluralize', + map: const { 'count': '=>count' }) +@NgDirective( + selector: '[ng-pluralize]', + map: const { 'count': '=>count' }) +class NgPluralizeDirective { + + final dom.Element element; + final Scope scope; + final Interpolate interpolate; + int offset; + Map whens = new Map(); + Map whensOffset = new Map(); + static final RegExp IS_WHEN = new RegExp(r'^when-(minus-)?.'); + + NgPluralizeDirective(this.scope, this.element, this.interpolate, NodeAttrs attributes) { + Map whens = attributes['when'] == null ? {} : scope.$eval(attributes['when']); + offset = attributes['offset'] == null ? 0 : int.parse(attributes['offset']); + + element.attributes.keys.where((k) => IS_WHEN.hasMatch(k)).forEach((k) { + var rule = k.replaceFirst('when-', '').replaceFirst('minus-', '-'); + whens[rule] = element.attributes[k]; + }); + + if (whens['other'] == null) { + throw "ngPluralize error! The 'other' plural category must always be specified"; + } + + whens.forEach((k, v) { + if (['zero', 'one', 'two', 'few', 'many', 'other'].contains(k)) { + this.whensOffset[new Symbol(k.toString())] = v; + } else { + this.whens[k.toString()] = v; + } + }); + } + + set count(value) { + if (value is! num) { + try { + value = int.parse(value); + } catch(e) { + try { + value = double.parse(value); + } + catch(e) { + element.text = ''; + return; + } + } + } + + String stringValue = value.toString(); + int intValue = value is double ? value.round() : value; + + if (whens[stringValue] != null) { + _setAndWatch(whens[stringValue]); + } else { + intValue -= offset; + var exp = Function.apply(Intl.plural, [intValue], whensOffset); + if (exp != null) { + exp = exp.replaceAll(r'{}', (value - offset).toString()); + _setAndWatch(exp); + } + } + } + + _setAndWatch(expression) { + Interpolation interpolation = interpolate(expression); + interpolation.setter = (text) => element.text = text; + interpolation.setter(expression); + scope.$watchSet(interpolation.watchExpressions, interpolation.call); + } +} diff --git a/test/directive/ng_pluralize_spec.dart b/test/directive/ng_pluralize_spec.dart new file mode 100644 index 000000000..88d492b69 --- /dev/null +++ b/test/directive/ng_pluralize_spec.dart @@ -0,0 +1,193 @@ +library ng_pluralize_spec; + +import '../_specs.dart'; + +main() { + describe('PluralizeDirective', () { + + describe('deal with pluralized strings without offset', () { + var element; + var elementAlt; + var elt; + TestBed _; + + beforeEach(inject((TestBed tb) { + _ = tb; + + element = _.compile( + '" + + '' + ); + + elementAlt = _.compile( + '

" + + '

' + ); + })); + + it('should show single/plural strings', () { + _.rootScope.email = '0'; + _.rootScope.$digest(); + expect(element.text).toEqual('You have no new email'); + expect(elementAlt.text).toEqual('You have no new email'); + + _.rootScope.email = '0'; + _.rootScope.$digest(); + expect(element.text).toEqual('You have no new email'); + expect(elementAlt.text).toEqual('You have no new email'); + + _.rootScope.email = 1; + _.rootScope.$digest(); + expect(element.text).toEqual('You have one new email'); + expect(elementAlt.text).toEqual('You have one new email'); + + _.rootScope.email = 0.01; + _.rootScope.$digest(); + expect(element.text).toEqual('You have 0.01 new emails'); + expect(elementAlt.text).toEqual('You have 0.01 new emails'); + + _.rootScope.email = '0.1'; + _.rootScope.$digest(); + expect(element.text).toEqual('You have 0.1 new emails'); + expect(elementAlt.text).toEqual('You have 0.1 new emails'); + + _.rootScope.email = 2; + _.rootScope.$digest(); + expect(element.text).toEqual('You have 2 new emails'); + expect(elementAlt.text).toEqual('You have 2 new emails'); + + _.rootScope.email = -0.1; + _.rootScope.$digest(); + expect(element.text).toEqual('You have -0.1 new emails'); + expect(elementAlt.text).toEqual('You have -0.1 new emails'); + + _.rootScope.email = '-0.01'; + _.rootScope.$digest(); + expect(element.text).toEqual('You have -0.01 new emails'); + expect(elementAlt.text).toEqual('You have -0.01 new emails'); + + _.rootScope.email = -2; + _.rootScope.$digest(); + expect(element.text).toEqual('You have -2 new emails'); + expect(elementAlt.text).toEqual('You have -2 new emails'); + + _.rootScope.email = -1; + _.rootScope.$digest(); + expect(element.text).toEqual('You have negative email. Whohoo!'); + expect(elementAlt.text).toEqual('You have negative email. Whohoo!'); + }); + + it('should show single/plural strings with mal-formed inputs', () { + _.rootScope.email = ''; + _.rootScope.$digest(); + expect(element.text).toEqual(''); + expect(elementAlt.text).toEqual(''); + + _.rootScope.email = null; + _.rootScope.$digest(); + expect(element.text).toEqual(''); + expect(elementAlt.text).toEqual(''); + + _.rootScope.email = 'a3'; + _.rootScope.$digest(); + expect(element.text).toEqual(''); + expect(elementAlt.text).toEqual(''); + + _.rootScope.email = '011'; + _.rootScope.$digest(); + expect(element.text).toEqual('You have 11 new emails'); + expect(elementAlt.text).toEqual('You have 11 new emails'); + + _.rootScope.email = '-011'; + _.rootScope.$digest(); + expect(element.text).toEqual('You have -11 new emails'); + expect(elementAlt.text).toEqual('You have -11 new emails'); + + _.rootScope.email = '1fff'; + _.rootScope.$digest(); + expect(element.text).toEqual(''); + expect(elementAlt.text).toEqual(''); + + _.rootScope.email = '0aa22'; + _.rootScope.$digest(); + expect(element.text).toEqual(''); + expect(elementAlt.text).toEqual(''); + + _.rootScope.email = '000001'; + _.rootScope.$digest(); + expect(element.text).toEqual('You have one new email'); + expect(elementAlt.text).toEqual('You have one new email'); + }); + }); + + describe('edge cases', () { + it('should be able to handle empty strings as possible values', (inject((TestBed _) { + var element = _.compile( + '" + + ''); + _.rootScope.email = '0'; + _.rootScope.$digest(); + expect(element.text).toEqual(''); + }))); + }); + + describe('deal with pluralized strings with offset', () { + it('should show single/plural strings with offset', (inject((TestBed _) { + var element = _.compile( + "" + + ""); + var elementAlt = _.compile( + "" + + ""); + _.rootScope.p1 = 'Igor'; + _.rootScope.p2 = 'Misko'; + + _.rootScope.viewCount = 0; + _.rootScope.$digest(); + expect(element.text).toEqual('Nobody is viewing.'); + expect(elementAlt.text).toEqual('Nobody is viewing.'); + + _.rootScope.viewCount = 1; + _.rootScope.$digest(); + expect(element.text).toEqual('Igor is viewing.'); + expect(elementAlt.text).toEqual('Igor is viewing.'); + + _.rootScope.viewCount = 2; + _.rootScope.$digest(); + expect(element.text).toEqual('Igor and Misko are viewing.'); + expect(elementAlt.text).toEqual('Igor and Misko are viewing.'); + + _.rootScope.viewCount = 3; + _.rootScope.$digest(); + expect(element.text).toEqual('Igor, Misko and one other person are viewing.'); + expect(elementAlt.text).toEqual('Igor, Misko and one other person are viewing.'); + + _.rootScope.viewCount = 4; + _.rootScope.$digest(); + expect(element.text).toEqual('Igor, Misko and 2 other people are viewing.'); + expect(elementAlt.text).toEqual('Igor, Misko and 2 other people are viewing.'); + }))); + }); + }); +}