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

Commit bed9fe1

Browse files
matskomhevery
authored andcommitted
feat(NgModel): introduce parsers and formatters
1 parent 085461d commit bed9fe1

File tree

4 files changed

+190
-28
lines changed

4 files changed

+190
-28
lines changed

lib/directive/ng_model.dart

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
part of angular.directive;
22

3+
abstract class NgModelConverter {
4+
String get name;
5+
parse(value) => value;
6+
format(value) => value;
7+
}
8+
9+
class _NoopModelConverter extends NgModelConverter {
10+
final name = 'ng-noop';
11+
}
12+
313
/**
414
* Ng-model directive is responsible for reading/writing to the model.
515
* The directive itself is headless. (It does not know how to render or what
@@ -15,13 +25,14 @@ class NgModel extends NgControl implements NgAttachAware {
1525
final AstParser _parser;
1626
final Scope _scope;
1727

18-
BoundGetter getter = ([_]) => null;
1928
BoundSetter setter = (_, [__]) => null;
2029

21-
var _lastValue;
30+
var _originalValue, _viewValue, _modelValue;
2231
String _exp;
2332
final _validators = <NgValidator>[];
33+
bool _alwaysProcessViewValue;
2434

35+
NgModelConverter _converter;
2536
Watch _removeWatch;
2637
bool _watchCollection;
2738
Function render = (value) => null;
@@ -32,11 +43,17 @@ class NgModel extends NgControl implements NgAttachAware {
3243
{
3344
_exp = attrs["ng-model"];
3445
watchCollection = false;
46+
47+
//Since the user will never be editing the value of a select element then
48+
//there is no reason to guard the formatter from changing the DOM value.
49+
_alwaysProcessViewValue = element.node.tagName == 'SELECT';
50+
converter = new _NoopModelConverter();
3551
}
3652

37-
void process(value, [_]) {
53+
void processViewValue(value) {
3854
validate();
39-
_scope.rootScope.domWrite(() => render(value));
55+
_viewValue = converter.format(value);
56+
_scope.rootScope.domWrite(() => render(_viewValue));
4057
}
4158

4259
void attach() {
@@ -45,16 +62,23 @@ class NgModel extends NgControl implements NgAttachAware {
4562

4663
void reset() {
4764
untouched = true;
48-
modelValue = _lastValue;
65+
processViewValue(_originalValue);
66+
modelValue = _originalValue;
4967
}
5068

5169
void onSubmit(bool valid) {
5270
super.onSubmit(valid);
5371
if (valid) {
54-
_lastValue = modelValue;
72+
_originalValue = modelValue;
5573
}
5674
}
5775

76+
get converter => _converter;
77+
set converter(NgModelConverter c) {
78+
_converter = c;
79+
processViewValue(modelValue);
80+
}
81+
5882
@NgAttr('name')
5983
get name => _name;
6084
set name(value) {
@@ -66,41 +90,57 @@ class NgModel extends NgControl implements NgAttachAware {
6690
get watchCollection => _watchCollection;
6791
set watchCollection(value) {
6892
if (_watchCollection == value) return;
93+
94+
var onChange = (value, [_]) {
95+
if (_alwaysProcessViewValue || _modelValue != value) {
96+
_modelValue = value;
97+
processViewValue(value);
98+
}
99+
};
100+
69101
_watchCollection = value;
70102
if (_removeWatch!=null) _removeWatch.remove();
71103
if (_watchCollection) {
72104
_removeWatch = _scope.watch(
73105
_parser(_exp, collection: true),
74106
(changeRecord, _) {
75-
var value = changeRecord is CollectionChangeRecord ? changeRecord.iterable: changeRecord;
76-
process(value);
107+
onChange(changeRecord is CollectionChangeRecord
108+
? changeRecord.iterable
109+
: changeRecord);
77110
});
78111
} else if (_exp != null) {
79-
_removeWatch = _scope.watch(_exp, process);
112+
_removeWatch = _scope.watch(_exp, onChange);
80113
}
81114
}
82115

83116
// TODO(misko): getters/setters need to go. We need AST here.
84117
@NgCallback('ng-model')
85118
set model(BoundExpression boundExpression) {
86-
getter = boundExpression;
87119
setter = boundExpression.assign;
88-
89120
_scope.rootScope.runAsync(() {
90-
_lastValue = modelValue;
121+
_modelValue = boundExpression();
122+
_originalValue = modelValue;
123+
processViewValue(_modelValue);
91124
});
92125
}
93126

94-
// TODO(misko): right now viewValue and modelValue are the same,
95-
// but this needs to be changed to support converters and form validation
96-
get viewValue => modelValue;
127+
get viewValue => _viewValue;
97128
set viewValue(value) {
129+
_viewValue = value;
98130
modelValue = value;
99-
value == _lastValue ? (pristine = true) : (dirty = true);
100131
}
101132

102-
get modelValue => getter();
103-
set modelValue(value) => setter(value);
133+
get modelValue => _modelValue;
134+
set modelValue(value) {
135+
try {
136+
value = converter.parse(value);
137+
} catch(e) {
138+
value = null;
139+
}
140+
_modelValue = value;
141+
setter(value);
142+
modelValue == _originalValue ? (pristine = true) : (dirty = true);
143+
}
104144

105145
get validators => _validators;
106146

@@ -110,7 +150,7 @@ class NgModel extends NgControl implements NgAttachAware {
110150
void validate() {
111151
if (validators.isNotEmpty) {
112152
validators.forEach((validator) {
113-
setValidity(validator.name, validator.isValid(viewValue));
153+
setValidity(validator.name, validator.isValid(modelValue));
114154
});
115155
} else {
116156
valid = true;

lib/directive/ng_model_validators.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
part of angular.directive;
22

33
abstract class NgValidator {
4-
final String name;
4+
String get name;
55
bool isValid(modelValue);
66
}
77

test/directive/ng_model_spec.dart

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1118,6 +1118,94 @@ void main() {
11181118
expect(model.touched).toBe(false);
11191119
expect(model.untouched).toBe(true);
11201120
});
1121+
1122+
describe('converters', () {
1123+
it('should parse the model value according to the given parser', inject((Scope scope) {
1124+
_.compile('<input type="text" ng-model="model" probe="i">');
1125+
scope.apply();
1126+
1127+
var probe = scope.context['i'];
1128+
var input = probe.element;
1129+
var model = probe.directive(NgModel);
1130+
model.converter = new LowercaseValueParser();
1131+
1132+
input.value = 'HELLO';
1133+
_.triggerEvent(input, 'change');
1134+
_.rootScope.apply();
1135+
1136+
expect(model.viewValue).toEqual('HELLO');
1137+
expect(model.modelValue).toEqual('hello');
1138+
}));
1139+
1140+
it('should format the model value according to the given formatter', inject((Scope scope) {
1141+
_.compile('<input type="text" ng-model="model" probe="i">');
1142+
scope.apply();
1143+
1144+
var probe = scope.context['i'];
1145+
var input = probe.element;
1146+
var model = probe.directive(NgModel);
1147+
model.converter = new UppercaseValueFormatter();
1148+
1149+
scope.apply(() {
1150+
scope.context['model'] = 'greetings';
1151+
});
1152+
1153+
expect(model.viewValue).toEqual('GREETINGS');
1154+
expect(model.modelValue).toEqual('greetings');
1155+
}));
1156+
1157+
it('should retain the current input value if the parser fails', inject((Scope scope) {
1158+
_.compile('<form name="myForm">' +
1159+
' <input type="text" ng-model="model1" name="myModel1" probe="i">' +
1160+
' <input type="text" ng-model="model2" name="myModel2" probe="j">' +
1161+
'</form>');
1162+
scope.apply();
1163+
1164+
var probe1 = scope.context['i'];
1165+
var input1 = probe1.element;
1166+
var model1 = probe1.directive(NgModel);
1167+
1168+
var probe2 = scope.context['j'];
1169+
var input2 = probe2.element;
1170+
var model2 = probe2.directive(NgModel);
1171+
1172+
model1.converter = new FailedValueParser();
1173+
1174+
input1.value = '123';
1175+
_.triggerEvent(input1, 'change');
1176+
_.rootScope.apply();
1177+
1178+
expect(model1.viewValue).toEqual('123');
1179+
expect(input1.value).toEqual('123');
1180+
expect(model1.modelValue).toEqual(null);
1181+
1182+
expect(model2.viewValue).toEqual(null);
1183+
expect(input2.value).toEqual('');
1184+
expect(model2.modelValue).toEqual(null);
1185+
}));
1186+
1187+
it('should reformat the viewValue when the formatter is changed', inject((Scope scope) {
1188+
_.compile('<input type="text" ng-model="model" probe="i">');
1189+
scope.apply();
1190+
1191+
var probe = scope.context['i'];
1192+
var input = probe.element;
1193+
var model = probe.directive(NgModel);
1194+
model.converter = new LowercaseValueParser();
1195+
1196+
input.value = 'HI THERE';
1197+
_.triggerEvent(input, 'change');
1198+
_.rootScope.apply();
1199+
1200+
expect(model.viewValue).toEqual('HI THERE');
1201+
expect(model.modelValue).toEqual('hi there');
1202+
1203+
model.converter = new VowelValueParser();
1204+
1205+
expect(model.viewValue).toEqual('iee');
1206+
expect(model.modelValue).toEqual('hi there');
1207+
}));
1208+
});
11211209
});
11221210
}
11231211

@@ -1127,3 +1215,39 @@ void main() {
11271215
class ControllerWithNoLove {
11281216
var apathy = null;
11291217
}
1218+
1219+
class LowercaseValueParser implements NgModelConverter {
1220+
final name = 'lowercase';
1221+
format(value) => value;
1222+
parse(value) {
1223+
return value != null ? value.toLowerCase() : null;
1224+
}
1225+
}
1226+
1227+
class UppercaseValueFormatter implements NgModelConverter {
1228+
final name = 'uppercase';
1229+
parse(value) => value;
1230+
format(value) {
1231+
return value != null ? value.toUpperCase() : null;
1232+
}
1233+
}
1234+
1235+
class FailedValueParser implements NgModelConverter {
1236+
final name = 'failed';
1237+
format(value) => value;
1238+
parse(value) {
1239+
throw new Exception();
1240+
}
1241+
}
1242+
1243+
class VowelValueParser implements NgModelConverter {
1244+
final name = 'vowel';
1245+
parse(value) => value;
1246+
format(value) {
1247+
if(value != null) {
1248+
var exp = new RegExp("[^aeiouAEIOU]");
1249+
value = value.replaceAll(exp, "");
1250+
}
1251+
return value;
1252+
}
1253+
}

test/directive/ng_model_validators_spec.dart

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ void main() {
2929
expect(model.valid).toEqual(false);
3030
expect(model.invalid).toEqual(true);
3131

32-
_.rootScope.context['val'] = 'value';
33-
model.validate();
32+
_.rootScope.apply(() {
33+
_.rootScope.context['val'] = 'value';
34+
});
3435

3536
expect(model.valid).toEqual(true);
3637
expect(model.invalid).toEqual(false);
@@ -42,12 +43,12 @@ void main() {
4243
Probe probe = _.rootScope.context['i'];
4344
var model = probe.directive(NgModel);
4445

45-
model.validate();
4646
expect(model.valid).toEqual(false);
4747
expect(model.invalid).toEqual(true);
4848

49-
_.rootScope.context['val'] = 5;
50-
model.validate();
49+
_.rootScope.apply(() {
50+
_.rootScope.context['val'] = 5;
51+
});
5152

5253
expect(model.valid).toEqual(true);
5354
expect(model.invalid).toEqual(false);
@@ -89,23 +90,20 @@ void main() {
8990
Probe probe = _.rootScope.context['i'];
9091
var model = probe.directive(NgModel);
9192

92-
model.validate();
9393
expect(model.valid).toEqual(true);
9494
expect(model.invalid).toEqual(false);
9595

9696
_.rootScope.apply(() {
9797
_.rootScope.context['val'] = 'googledotcom';
9898
});
9999

100-
model.validate();
101100
expect(model.valid).toEqual(false);
102101
expect(model.invalid).toEqual(true);
103102

104103
_.rootScope.apply(() {
105104
_.rootScope.context['val'] = 'http://www.google.com';
106105
});
107106

108-
model.validate();
109107
expect(model.valid).toEqual(true);
110108
expect(model.invalid).toEqual(false);
111109
}));

0 commit comments

Comments
 (0)