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

Commit

Permalink
feat(manualModelWatcher): add alternatives to global model watcher
Browse files Browse the repository at this point in the history
- add manualModelWatcher option that accepts boolean or custom watcher function for more flexibilty
- add watchAllExpressions option (boolean) that puts angular watcher on every field expression
- add runFieldExpressions option to custom field watcher that runs field expressions on expression change
  • Loading branch information
kwypchlo committed Jan 19, 2016
1 parent 5dbfbb8 commit 10905d6
Show file tree
Hide file tree
Showing 4 changed files with 413 additions and 25 deletions.
18 changes: 17 additions & 1 deletion src/directives/formly-field.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ function formlyField($http, $q, $compile, $templateCache, $interpolate, formlyCo

// @ngInject
function FormlyFieldController($scope, $timeout, $parse, $controller, formlyValidationMessages) {
/* eslint max-statements:[2, 32] */
/* eslint max-statements:[2, 34] */
if ($scope.options.fieldGroup) {
setupFieldGroup()
return
Expand All @@ -53,6 +53,7 @@ function formlyField($http, $q, $compile, $templateCache, $interpolate, formlyCo
setDefaultValue()
setInitialValue()
runExpressions()
watchExpressions()
addValidationMessages($scope.options)
invokeControllers($scope, $scope.options, fieldType)

Expand All @@ -72,6 +73,21 @@ function formlyField($http, $q, $compile, $templateCache, $interpolate, formlyCo
}, 0, false)
}

function watchExpressions() {
if ($scope.formOptions.watchAllExpressions) {
const field = $scope.options
const currentValue = valueGetterSetter()
angular.forEach(field.expressionProperties, function watchExpression(expression, prop) {
const setter = $parse(prop).assign
$scope.$watch(function expressionPropertyWatcher() {
return formlyUtil.formlyEval($scope, expression, currentValue, currentValue)
}, function expressionPropertyListener(value) {
setter(field, value)
}, true)
})
}
}

function valueGetterSetter(newVal) {
if (!$scope.model || !$scope.options.key) {
return undefined
Expand Down
76 changes: 53 additions & 23 deletions src/directives/formly-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,28 +112,35 @@ function formlyForm(formlyUsability, formlyWarn, $parse, formlyConfig, $interpol
setupFields()

// watch the model and evaluate watch expressions that depend on it.
$scope.$watch('model', onModelOrFormStateChange, true)
if (!$scope.options.manualModelWatcher) {
$scope.$watch('model', onModelOrFormStateChange, true)
} else if (angular.isFunction($scope.options.manualModelWatcher)) {
$scope.$watch($scope.options.manualModelWatcher, 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()
}
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 All @@ -158,6 +165,10 @@ function formlyForm(formlyUsability, formlyWarn, $parse, formlyConfig, $interpol

setupModels()

if ($scope.options.watchAllExpressions) {
angular.forEach($scope.fields, setupExpressionWatchers)
}

angular.forEach($scope.fields, attachKey) // attaches a key based on the index if a key isn't specified
angular.forEach($scope.fields, setupWatchers) // setup watchers for all fields
}
Expand Down Expand Up @@ -217,6 +228,8 @@ function formlyForm(formlyUsability, formlyWarn, $parse, formlyConfig, $interpol
function setupModels() {
// a set of field models that are already watched (the $scope.model will have its own watcher)
const watchedModels = [$scope.model]
// we will not set up automatic model watchers if manual mode is set
const manualModelWatcher = $scope.options.manualModelWatcher

if ($scope.options.formState) {
// $scope.options.formState will have its own watcher
Expand All @@ -226,21 +239,31 @@ function formlyForm(formlyUsability, formlyWarn, $parse, formlyConfig, $interpol
angular.forEach($scope.fields, (field) => {
const isNewModel = initModel(field)

if (field.model && isNewModel && watchedModels.indexOf(field.model) === -1) {
if (field.model && isNewModel && watchedModels.indexOf(field.model) === -1 && !manualModelWatcher) {
$scope.$watch(() => field.model, onModelOrFormStateChange, true)
watchedModels.push(field.model)
}
})
}

function setupExpressionWatchers(field, index) {
if (field.hideExpression) { // can't use hide with expressionProperties reliably
const model = field.model || $scope.model
$scope.$watch(function hideExpressionWatcher() {
const val = model[field.key]
return evalCloseToFormlyExpression(field.hideExpression, val, field, index)
}, (hide) => field.hide = hide, true)
}
}

function initModel(field) {
let isNewModel = true

if (angular.isString(field.model)) {
const expression = field.model
const index = $scope.fields.indexOf(field)

isNewModel = !refrencesCurrentlyWatchedModel(expression)
isNewModel = !referencesCurrentlyWatchedModel(expression)

field.model = evalCloseToFormlyExpression(expression, undefined, field, index)
if (!field.model) {
Expand All @@ -254,7 +277,7 @@ function formlyForm(formlyUsability, formlyWarn, $parse, formlyConfig, $interpol
return isNewModel
}

function refrencesCurrentlyWatchedModel(expression) {
function referencesCurrentlyWatchedModel(expression) {
return ['model', 'formState'].some(item => {
return formlyUtil.startsWith(expression, `${item}.`) || formlyUtil.startsWith(expression, `${item}[`)
})
Expand All @@ -275,7 +298,7 @@ function formlyForm(formlyUsability, formlyWarn, $parse, formlyConfig, $interpol
watchers = [watchers]
}
angular.forEach(watchers, function setupWatcher(watcher) {
if (!angular.isDefined(watcher.listener)) {
if (!angular.isDefined(watcher.listener) && !watcher.runFieldExpressions) {
throw formlyUsability.getFieldError(
'all-field-watchers-must-have-a-listener',
'All field watchers must have a listener', field
Expand Down Expand Up @@ -306,13 +329,20 @@ function formlyForm(formlyUsability, formlyWarn, $parse, formlyConfig, $interpol

function getWatchListener(watcher, field, index) {
let watchListener = watcher.listener
if (angular.isFunction(watchListener)) {
if (angular.isFunction(watchListener) || watcher.runFieldExpressions) {
// wrap the field's watch listener so we can call it with the field as the first arg
// and the stop function as the last arg as a helper
const originalListener = watchListener
watchListener = function formlyWatchListener() {
const args = modifyArgs(watcher, index, ...arguments)
return originalListener(...args)
let value
if (originalListener) {
const args = modifyArgs(watcher, index, ...arguments)
value = originalListener(...args)
}
if (watcher.runFieldExpressions) {
runFieldExpressionProperties(field, index)
}
return value
}
watchListener.displayName = `Formly Watch Listener for field for ${field.key}`
}
Expand Down
Loading

0 comments on commit 10905d6

Please sign in to comment.