Skip to content
This repository has been archived by the owner on Feb 22, 2018. It is now read-only.

Commit

Permalink
feat(ng-pluralize): Implement the ng-pluralize directive
Browse files Browse the repository at this point in the history
  • Loading branch information
vicb authored and mhevery committed Jan 7, 2014
1 parent 6549c98 commit 51d951e
Show file tree
Hide file tree
Showing 3 changed files with 350 additions and 0 deletions.
3 changes: 3 additions & 0 deletions lib/directive/module.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -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);
Expand Down
154 changes: 154 additions & 0 deletions lib/directive/ng_pluralize.dart
Original file line number Diff line number Diff line change
@@ -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:
*
* <ng-pluralize count="personCount"
* when="{'0': 'Nobody is viewing.',
* 'one': '1 person is viewing.',
* 'other': '{} people are viewing.'}">
* </ng-pluralize>
*
* 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:
*
* <ng-pluralize count="personCount" offset=2
* when="{'0': 'Nobody is viewing.',
* '1': '{{person1}} is viewing.',
* '2': '{{person1}} and {{person2}} are viewing.',
* 'one': '{{person1}}, {{person2}} and one other person are viewing.',
* 'other': '{{person1}}, {{person2}} and {} other people are viewing.'}">
* </ng-pluralize>
*
* 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<String, String> whens = new Map();
Map<Symbol, String> whensOffset = new Map();
static final RegExp IS_WHEN = new RegExp(r'^when-(minus-)?.');

NgPluralizeDirective(this.scope, this.element, this.interpolate, NodeAttrs attributes) {
Map<String, String> 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);
}
}
193 changes: 193 additions & 0 deletions test/directive/ng_pluralize_spec.dart
Original file line number Diff line number Diff line change
@@ -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(
'<ng-pluralize count="email"' +
"when=\"{'-1': 'You have negative email. Whohoo!'," +
"'0': 'You have no new email'," +
"'one': 'You have one new email'," +
"'other': 'You have {} new emails'}\">" +
'</ng-pluralize>'
);

elementAlt = _.compile(
'<p ng-pluralize count="email" ' +
"when-minus-1='You have negative email. Whohoo!' " +
"when-0='You have no new email' " +
"when-one='You have one new email' " +
"when-other='You have {} new emails'>" +
'</p>'
);
}));

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(
'<ng-pluralize count="email"' +
"when=\"{'0': ''," +
"'one': 'Some text'," +
"'other': 'Some text'}\">" +
'</ng-pluralize>');
_.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(
"<ng-pluralize count='viewCount' offset='2' " +
"when=\"{'0': 'Nobody is viewing.'," +
"'1': '{{p1}} is viewing.'," +
"'2': '{{p1}} and {{p2}} are viewing.'," +
"'one': '{{p1}}, {{p2}} and one other person are viewing.'," +
"'other': '{{p1}}, {{p2}} and {} other people are viewing.'}\">" +
"</ng-pluralize>");
var elementAlt = _.compile(
"<ng-pluralize count='viewCount' offset='2' " +
"when-0='Nobody is viewing.'" +
"when-1='{{p1}} is viewing.'" +
"when-2='{{p1}} and {{p2}} are viewing.'" +
"when-one='{{p1}}, {{p2}} and one other person are viewing.'" +
"when-other='{{p1}}, {{p2}} and {} other people are viewing.'>" +
"</ng-pluralize>");
_.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.');
})));
});
});
}

0 comments on commit 51d951e

Please sign in to comment.