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

Commit 8624f23

Browse files
committed
Fixes #426: Support for nested model attribute (watcher optimization)
1 parent a60ef3c commit 8624f23

File tree

4 files changed

+94
-4
lines changed

4 files changed

+94
-4
lines changed

src/directives/formly-form.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,20 +186,32 @@ function formlyForm(formlyUsability, formlyWarn, $parse, formlyConfig, $interpol
186186
// a set of field models that are already watched (the $scope.model will have its own watcher)
187187
const watchedModels = [$scope.model];
188188

189+
if ($scope.options.formState) {
190+
// $scope.options.formState will have its own watcher
191+
watchedModels.push($scope.options.formState);
192+
}
193+
189194
angular.forEach($scope.fields, (field) => {
190-
initModel(field);
195+
const isNewModel = initModel(field);
191196

192-
if (field.model && watchedModels.indexOf(field.model) === -1) {
197+
if (field.model && isNewModel && watchedModels.indexOf(field.model) === -1) {
193198
$scope.$watch(() => field.model, onModelOrFormStateChange, true);
194199
watchedModels.push(field.model);
195200
}
196201
});
197202
}
198203

199204
function initModel(field) {
205+
let isNewModel = true;
206+
200207
if (angular.isString(field.model)) {
201208
const expression = field.model;
202209
const index = $scope.fields.indexOf(field);
210+
211+
if (formlyUtil.startsWith(expression, 'model.') || formlyUtil.startsWith(expression, 'formState.')) {
212+
isNewModel = false;
213+
}
214+
203215
field.model = evalCloseToFormlyExpression(expression, undefined, field, index);
204216
if (!field.model) {
205217
throw formlyUsability.getFieldError(
@@ -209,6 +221,7 @@ function formlyForm(formlyUsability, formlyWarn, $parse, formlyConfig, $interpol
209221
field);
210222
}
211223
}
224+
return isNewModel;
212225
}
213226

214227
function attachKey(field, index) {

src/directives/formly-form.test.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,58 @@ describe('formly-form', () => {
406406
});
407407
});
408408

409+
describe('nested model as string', () => {
410+
let spy;
411+
412+
beforeEach(() => {
413+
spy = sinon.spy();
414+
415+
scope.model = {
416+
nested: {}
417+
};
418+
419+
scope.fields = [
420+
{template: input, key: 'foo'}
421+
];
422+
});
423+
424+
it('starting with "model." should be assigned with only one watcher', () => {
425+
scope.fields[0].model = 'model.nested';
426+
427+
compileAndDigest();
428+
$timeout.flush();
429+
430+
scope.fields[0].expressionProperties = {'data.dummy': spy};
431+
432+
scope.model.nested.foo = 'value';
433+
scope.$digest();
434+
$timeout.flush();
435+
436+
expect(spy).to.have.been.calledOnce;
437+
});
438+
439+
it('starting with "formState." should be assigned with only one watcher', () => {
440+
const formWithOptions = '<formly-form model="model" fields="fields" options="options"></formly-form>';
441+
scope.options = {
442+
formState: {
443+
nested: {}
444+
}
445+
};
446+
scope.fields[0].model = 'formState.nested';
447+
448+
compileAndDigest(formWithOptions);
449+
$timeout.flush();
450+
451+
scope.fields[0].expressionProperties = {'data.dummy': spy};
452+
453+
scope.options.formState.nested.foo = 'value';
454+
scope.$digest();
455+
$timeout.flush();
456+
457+
expect(spy).to.have.been.calledOnce;
458+
});
459+
});
460+
409461
describe('hideExpression', () => {
410462
beforeEach(() => {
411463
scope.model = {};

src/other/utils.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import angular from 'angular-fix';
22

3-
export default {formlyEval, getFieldId, reverseDeepMerge, findByNodeName, arrayify, extendFunction, extendArray};
3+
export default {formlyEval, getFieldId, reverseDeepMerge, findByNodeName, arrayify, extendFunction, extendArray, startsWith};
44

55
function formlyEval(scope, expression, $modelValue, $viewValue, extraLocals) {
66
if (angular.isFunction(expression)) {
@@ -102,3 +102,10 @@ function extendArray(primary, secondary, property) {
102102
}
103103
}
104104

105+
function startsWith(str, search) {
106+
if (angular.isString(str) && angular.isString(search)) {
107+
return str.length >= search.length && str.substring(0, search.length) === search;
108+
} else {
109+
return false;
110+
}
111+
}

src/other/utils.test.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import utils from './utils.js';
33

44
// gotta do this because webstorm/jshint doesn't like destructuring imports :-(
5-
const {extendFunction} = utils;
5+
const {extendFunction, startsWith} = utils;
66

77

88
describe(`utils`, () => {
@@ -23,4 +23,22 @@ describe(`utils`, () => {
2323
});
2424
});
2525

26+
describe(`startsWith`, () => {
27+
it(`should return true if a string has a given prefix`, () => {
28+
expect(startsWith('fooBar', 'foo')).to.be.true;
29+
});
30+
31+
it(`should return false if a string does not have a given prefix`, () => {
32+
expect(startsWith('fooBar', 'nah')).to.be.false;
33+
});
34+
35+
it(`should return false if no a string`, () => {
36+
expect(startsWith(undefined, 'foo')).to.be.false;
37+
expect(startsWith(5, 'foo')).to.be.false;
38+
expect(startsWith('foo', undefined)).to.be.false;
39+
expect(startsWith('foo', 5)).to.be.false;
40+
expect(startsWith(undefined, undefined)).to.be.false;
41+
});
42+
});
43+
2644
});

0 commit comments

Comments
 (0)