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

Commit

Permalink
adds manualModelWatcher option
Browse files Browse the repository at this point in the history
  • Loading branch information
Wypchlo, Karol (Consultant) authored and Wypchlo, Karol (Consultant) committed Jan 10, 2016
1 parent 5dbfbb8 commit 1c7bc62
Show file tree
Hide file tree
Showing 3 changed files with 311 additions and 19 deletions.
81 changes: 62 additions & 19 deletions src/directives/formly-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,29 +111,69 @@ function formlyForm(formlyUsability, formlyWarn, $parse, formlyConfig, $interpol
$scope.model = $scope.model || {}
setupFields()

// watch the model and evaluate watch expressions that depend on it.
$scope.$watch('model', onModelOrFormStateChange, true)
if ($scope.options.manualModelWatcher) {
setManualModelWatcher()
} else {
// watch the model and evaluate watch expressions that depend on it.
$scope.$watch('model', onModelOrFormStateChange, true)
}

if ($scope.options.formState) {
$scope.$watch('options.formState', onModelOrFormStateChange, true)
}

function onModelOrFormStateChange() {
angular.forEach($scope.fields, function runFieldExpressionProperties(field, index) {
const model = field.model || $scope.model
const promise = field.runExpressions && field.runExpressions()
if (field.hideExpression) { // can't use hide with expressionProperties reliably
const val = model[field.key]
field.hide = evalCloseToFormlyExpression(field.hideExpression, val, field, index)
}
if (field.extras && field.extras.validateOnModelChange && field.formControl) {
const validate = field.formControl.$validate
if (promise) {
promise.then(validate)
} else {
validate()
}
function setManualModelWatcher() {
const groupedWatchers = []

angular.forEach($scope.fields, function(field, index) {
if (field.extras && Array.isArray(field.extras.watch)) {
angular.forEach(field.extras.watch, function(expression) {
const model = field.model || $scope.model
let watcherGroup = groupedWatchers.find((watcher) => {
return watcher.expression === expression && watcher.model === model
})

if (!watcherGroup) {
watcherGroup = {model, expression, callbacks: []}
groupedWatchers.push(watcherGroup)
}

if (angular.isString(expression) && field.model) {
watcherGroup.parsedExpression = $parse(expression).bind(null, $scope, {model: field.model})
}

watcherGroup.callbacks.push(runFieldExpressionProperties.bind(null, field, index))
})
}
})

angular.forEach(groupedWatchers, function(watcher) {
const expression = watcher.parsedExpression || watcher.expression
$scope.$watch(expression, function manualFieldModelWatcher() {
angular.forEach(watcher.callbacks, (callback) => callback())
}, true)
})
}

function onModelOrFormStateChange() {
angular.forEach($scope.fields, runFieldExpressionProperties)
}

function runFieldExpressionProperties(field, index) {
const model = field.model || $scope.model
const promise = field.runExpressions && field.runExpressions()
if (field.hideExpression) { // can't use hide with expressionProperties reliably
const val = model[field.key]
field.hide = evalCloseToFormlyExpression(field.hideExpression, val, field, index)
}
if (field.extras && field.extras.validateOnModelChange && field.formControl) {
const validate = field.formControl.$validate
if (promise) {
promise.then(validate)
} else {
validate()
}
}
}

function setupFields() {
Expand Down Expand Up @@ -227,8 +267,11 @@ function formlyForm(formlyUsability, formlyWarn, $parse, formlyConfig, $interpol
const isNewModel = initModel(field)

if (field.model && isNewModel && watchedModels.indexOf(field.model) === -1) {
$scope.$watch(() => field.model, onModelOrFormStateChange, true)
watchedModels.push(field.model)
// don't set up automatic model watchers if manual mode is set
if (!$scope.options.manualModelWatcher) {
$scope.$watch(() => field.model, onModelOrFormStateChange, true)
watchedModels.push(field.model)
}
}
})
}
Expand Down
245 changes: 245 additions & 0 deletions src/directives/formly-form.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -953,4 +953,249 @@ describe('formly-form', () => {
expect(expression).to.have.been.called
})
})

describe(`manualModelWatcher options`, () => {
beforeEach(() => {
scope.model = {
foo: 'myFoo',
bar: 123,
}

scope.fields = [
{template: input, key: 'foo'},
{template: input, key: 'bar', templateOptions: {type: 'number'}},
]

scope.options = {
manualModelWatcher: true,
}
})

it(`should block a global model watcher`, () => {
const spy = sinon.spy()

scope.fields[0].expressionProperties = {
'templateOptions.label': spy,
}

compileAndDigest()
$timeout.flush()

spy.reset()

scope.model.foo = 'bar'

scope.$digest()
$timeout.verifyNoPendingTasks()

expect(spy).to.not.have.been.called
})

it(`should watch manually selected model property`, () => {
const spy = sinon.spy()

scope.fields[0].extras = {
watch: [
'model.foo',
],
}
scope.fields[0].expressionProperties = {
'templateOptions.label': spy,
}

compileAndDigest()
$timeout.flush()

spy.reset()

scope.model.foo = 'bar'

scope.$digest()
$timeout.flush()

expect(spy).to.have.been.called
})

it(`should not watch model properties that do not have manual watcher defined`, () => {
const spy = sinon.spy()

scope.fields[0].extras = {
watch: [
'model.foo',
],
}
scope.fields[0].expressionProperties = {
'templateOptions.label': spy,
}

compileAndDigest()
$timeout.flush()

spy.reset()

scope.model.bar = 123

scope.$digest()
$timeout.verifyNoPendingTasks()

expect(spy).to.not.have.been.called
})

it(`should run manual watchers defined as a function`, () => {
const spy = sinon.spy()
const stub = sinon.stub()

scope.fields[0].extras = {
watch: [
stub,
],
}
scope.fields[0].expressionProperties = {
'templateOptions.label': spy,
}

compileAndDigest()
$timeout.flush()

stub.reset()
spy.reset()

// set random stub value so it triggers watcher function
stub.returns(Math.random())

scope.$digest()
$timeout.flush()

expect(stub).to.have.been.called
expect(spy).to.have.been.called
})

it('should not trigger watches on other fields', () => {
const spy1 = sinon.spy()
const spy2 = sinon.spy()

scope.fields[0].extras = {
watch: [
'model.foo',
],
}
scope.fields[0].expressionProperties = {
'templateOptions.label': spy1,
}
scope.fields[1].expressionProperties = {
'templateOptions.label': spy2,
}

compileAndDigest()
$timeout.flush()

spy1.reset()
spy2.reset()

scope.model.foo = 'asd'

scope.$digest()
$timeout.flush()

expect(spy1).to.have.been.called
expect(spy2).to.not.have.been.called
})

it('works with models that are declared as string (relative model)', () => {
const spy = sinon.spy()
const model = 'model.nested'

scope.model = {
nested: {
foo: 'foo',
},
}
scope.fields[0].model = model
scope.fields[0].extras = {
watch: [
'model.foo',
],
}
scope.fields[0].expressionProperties = {
'templateOptions.label': spy,
}

compileAndDigest()
$timeout.flush()

spy.reset()

scope.model.nested.foo = 'bar'

scope.$digest()
$timeout.flush()

expect(spy).to.have.been.called
})

it('works with models that are declared as object', () => {
const spy = sinon.spy()
const model = {
foo: 'foo',
}

//scope.options.manualModelWatcher = false

scope.fields[0].model = model
scope.fields[0].extras = {
watch: [
'model.foo',
],
}
scope.fields[0].expressionProperties = {
'templateOptions.label': spy,
}

compileAndDigest()
$timeout.flush()

spy.reset()

model.foo = 'bar'

scope.$digest()
$timeout.flush()

expect(spy).to.have.been.called
})

it('groups same expressions on fields with same model and executes multiple callbacks', () => {
const spyWatcher = sinon.stub()
const spyCallback1 = sinon.stub()
const spyCallback2 = sinon.stub()

scope.fields[0].extras = {
watch: [spyWatcher],
}
scope.fields[0].expressionProperties = {
'templateOptions.label': spyCallback1,
}

scope.fields[1].extras = {
watch: [spyWatcher],
}
scope.fields[1].expressionProperties = {
'templateOptions.label': spyCallback2,
}

compileAndDigest()
$timeout.flush()

spyWatcher.reset().returns(Math.random())
spyCallback1.reset()
spyCallback2.reset()

scope.$digest()
$timeout.flush()

expect(spyWatcher).to.have.been.called.once
expect(spyCallback1).to.have.been.called
expect(spyCallback2).to.have.been.called
})
})
})
4 changes: 4 additions & 0 deletions src/providers/formlyApiCheck.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ const fieldOptionsApiShape = {
expressionProperties: expressionProperties.optional,
extras: apiCheck.shape({
validateOnModelChange: apiCheck.bool.optional,
watch: apiCheck.arrayOf(apiCheck.oneOfType([
apiCheck.string, apiCheck.func,
])).optional,
skipNgModelAttrsManipulator: apiCheck.oneOfType([
apiCheck.string, apiCheck.bool,
]).optional,
Expand Down Expand Up @@ -164,6 +167,7 @@ const formOptionsApi = apiCheck.shape({
updateInitialValue: apiCheck.func.optional,
removeChromeAutoComplete: apiCheck.bool.optional,
templateManipulators: templateManipulators.optional,
manualModelWatcher: apiCheck.bool.optional,
wrapper: specifyWrapperType.optional,
fieldTransform: apiCheck.oneOfType([
apiCheck.func, apiCheck.array,
Expand Down

0 comments on commit 1c7bc62

Please sign in to comment.