|
| 1 | +import angular from 'angular-fix' |
| 2 | + |
| 3 | +function isFieldGroup(field) { |
| 4 | + return field && !!field.fieldGroup |
| 5 | +} |
| 6 | + |
| 7 | +// @ngInject |
| 8 | +export default function FormlyFormController( |
| 9 | + formlyUsability, formlyWarn, formlyConfig, $parse, $scope, formlyApiCheck, formlyUtil) { |
| 10 | + |
| 11 | + setupOptions() |
| 12 | + $scope.model = $scope.model || {} |
| 13 | + setupFields() |
| 14 | + |
| 15 | + // watch the model and evaluate watch expressions that depend on it. |
| 16 | + if (!$scope.options.manualModelWatcher) { |
| 17 | + $scope.$watch('model', onModelOrFormStateChange, true) |
| 18 | + } else if (angular.isFunction($scope.options.manualModelWatcher)) { |
| 19 | + $scope.$watch($scope.options.manualModelWatcher, onModelOrFormStateChange, true) |
| 20 | + } |
| 21 | + |
| 22 | + if ($scope.options.formState) { |
| 23 | + $scope.$watch('options.formState', onModelOrFormStateChange, true) |
| 24 | + } |
| 25 | + |
| 26 | + function onModelOrFormStateChange() { |
| 27 | + angular.forEach($scope.fields, runFieldExpressionProperties) |
| 28 | + } |
| 29 | + |
| 30 | + function validateFormControl(formControl, promise) { |
| 31 | + const validate = formControl.$validate |
| 32 | + if (promise) { |
| 33 | + promise.then(validate) |
| 34 | + } else { |
| 35 | + validate() |
| 36 | + } |
| 37 | + } |
| 38 | + |
| 39 | + function runFieldExpressionProperties(field, index) { |
| 40 | + const model = field.model || $scope.model |
| 41 | + const promise = field.runExpressions && field.runExpressions() |
| 42 | + if (field.hideExpression) { // can't use hide with expressionProperties reliably |
| 43 | + const val = model[field.key] |
| 44 | + field.hide = evalCloseToFormlyExpression(field.hideExpression, val, field, index, {model}) |
| 45 | + } |
| 46 | + if (field.extras && field.extras.validateOnModelChange && field.formControl) { |
| 47 | + if (angular.isArray(field.formControl)) { |
| 48 | + angular.forEach(field.formControl, function(formControl) { |
| 49 | + validateFormControl(formControl, promise) |
| 50 | + }) |
| 51 | + } else { |
| 52 | + validateFormControl(field.formControl, promise) |
| 53 | + } |
| 54 | + } |
| 55 | + } |
| 56 | + |
| 57 | + function setupFields() { |
| 58 | + $scope.fields = $scope.fields || [] |
| 59 | + |
| 60 | + checkDeprecatedOptions($scope.options) |
| 61 | + |
| 62 | + let fieldTransforms = $scope.options.fieldTransform || formlyConfig.extras.fieldTransform |
| 63 | + |
| 64 | + if (!angular.isArray(fieldTransforms)) { |
| 65 | + fieldTransforms = [fieldTransforms] |
| 66 | + } |
| 67 | + |
| 68 | + angular.forEach(fieldTransforms, function transformFields(fieldTransform) { |
| 69 | + if (fieldTransform) { |
| 70 | + $scope.fields = fieldTransform($scope.fields, $scope.model, $scope.options, $scope.form) |
| 71 | + if (!$scope.fields) { |
| 72 | + throw formlyUsability.getFormlyError('fieldTransform must return an array of fields') |
| 73 | + } |
| 74 | + } |
| 75 | + }) |
| 76 | + |
| 77 | + setupModels() |
| 78 | + |
| 79 | + if ($scope.options.watchAllExpressions) { |
| 80 | + angular.forEach($scope.fields, setupHideExpressionWatcher) |
| 81 | + } |
| 82 | + |
| 83 | + angular.forEach($scope.fields, attachKey) // attaches a key based on the index if a key isn't specified |
| 84 | + angular.forEach($scope.fields, setupWatchers) // setup watchers for all fields |
| 85 | + } |
| 86 | + |
| 87 | + function checkDeprecatedOptions(options) { |
| 88 | + if (formlyConfig.extras.fieldTransform && angular.isFunction(formlyConfig.extras.fieldTransform)) { |
| 89 | + formlyWarn( |
| 90 | + 'fieldtransform-as-a-function-deprecated', |
| 91 | + 'fieldTransform as a function has been deprecated.', |
| 92 | + `Attempted for formlyConfig.extras: ${formlyConfig.extras.fieldTransform.name}`, |
| 93 | + formlyConfig.extras |
| 94 | + ) |
| 95 | + } else if (options.fieldTransform && angular.isFunction(options.fieldTransform)) { |
| 96 | + formlyWarn( |
| 97 | + 'fieldtransform-as-a-function-deprecated', |
| 98 | + 'fieldTransform as a function has been deprecated.', |
| 99 | + `Attempted for form`, |
| 100 | + options |
| 101 | + ) |
| 102 | + } |
| 103 | + } |
| 104 | + |
| 105 | + function setupOptions() { |
| 106 | + formlyApiCheck.throw( |
| 107 | + [formlyApiCheck.formOptionsApi.optional], [$scope.options], {prefix: 'formly-form options check'} |
| 108 | + ) |
| 109 | + $scope.options = $scope.options || {} |
| 110 | + $scope.options.formState = $scope.options.formState || {} |
| 111 | + |
| 112 | + angular.extend($scope.options, { |
| 113 | + updateInitialValue, |
| 114 | + resetModel, |
| 115 | + }) |
| 116 | + |
| 117 | + } |
| 118 | + |
| 119 | + function updateInitialValue() { |
| 120 | + angular.forEach($scope.fields, field => { |
| 121 | + if (isFieldGroup(field) && field.options) { |
| 122 | + field.options.updateInitialValue() |
| 123 | + } else { |
| 124 | + field.updateInitialValue() |
| 125 | + } |
| 126 | + }) |
| 127 | + } |
| 128 | + |
| 129 | + function resetModel() { |
| 130 | + angular.forEach($scope.fields, field => { |
| 131 | + if (isFieldGroup(field) && field.options) { |
| 132 | + field.options.resetModel() |
| 133 | + } else if (field.resetModel) { |
| 134 | + field.resetModel() |
| 135 | + } |
| 136 | + }) |
| 137 | + } |
| 138 | + |
| 139 | + function setupModels() { |
| 140 | + // a set of field models that are already watched (the $scope.model will have its own watcher) |
| 141 | + const watchedModels = [$scope.model] |
| 142 | + // we will not set up automatic model watchers if manual mode is set |
| 143 | + const manualModelWatcher = $scope.options.manualModelWatcher |
| 144 | + |
| 145 | + if ($scope.options.formState) { |
| 146 | + // $scope.options.formState will have its own watcher |
| 147 | + watchedModels.push($scope.options.formState) |
| 148 | + } |
| 149 | + |
| 150 | + angular.forEach($scope.fields, (field) => { |
| 151 | + const isNewModel = initModel(field) |
| 152 | + |
| 153 | + if (field.model && isNewModel && watchedModels.indexOf(field.model) === -1 && !manualModelWatcher) { |
| 154 | + $scope.$watch(() => field.model, onModelOrFormStateChange, true) |
| 155 | + watchedModels.push(field.model) |
| 156 | + } |
| 157 | + }) |
| 158 | + } |
| 159 | + |
| 160 | + function setupHideExpressionWatcher(field, index) { |
| 161 | + if (field.hideExpression) { // can't use hide with expressionProperties reliably |
| 162 | + const model = field.model || $scope.model |
| 163 | + $scope.$watch(function hideExpressionWatcher() { |
| 164 | + const val = model[field.key] |
| 165 | + return evalCloseToFormlyExpression(field.hideExpression, val, field, index, {model}) |
| 166 | + }, (hide) => field.hide = hide, true) |
| 167 | + } |
| 168 | + } |
| 169 | + |
| 170 | + function initModel(field) { |
| 171 | + let isNewModel = true |
| 172 | + |
| 173 | + if (angular.isString(field.model)) { |
| 174 | + const expression = field.model |
| 175 | + |
| 176 | + isNewModel = !referencesCurrentlyWatchedModel(expression) |
| 177 | + |
| 178 | + field.model = resolveStringModel(expression) |
| 179 | + |
| 180 | + $scope.$watch(() => resolveStringModel(expression), (model) => field.model = model) |
| 181 | + } |
| 182 | + |
| 183 | + return isNewModel |
| 184 | + |
| 185 | + function resolveStringModel(expression) { |
| 186 | + const index = $scope.fields.indexOf(field) |
| 187 | + const model = evalCloseToFormlyExpression(expression, undefined, field, index, {model: $scope.model}) |
| 188 | + |
| 189 | + if (!model) { |
| 190 | + throw formlyUsability.getFieldError( |
| 191 | + 'field-model-must-be-initialized', |
| 192 | + 'Field model must be initialized. When specifying a model as a string for a field, the result of the' + |
| 193 | + ' expression must have been initialized ahead of time.', |
| 194 | + field) |
| 195 | + } |
| 196 | + |
| 197 | + return model |
| 198 | + } |
| 199 | + } |
| 200 | + |
| 201 | + function referencesCurrentlyWatchedModel(expression) { |
| 202 | + return ['model', 'formState'].some(item => { |
| 203 | + return formlyUtil.startsWith(expression, `${item}.`) || formlyUtil.startsWith(expression, `${item}[`) |
| 204 | + }) |
| 205 | + } |
| 206 | + |
| 207 | + function attachKey(field, index) { |
| 208 | + if (!isFieldGroup(field)) { |
| 209 | + field.key = field.key || index || 0 |
| 210 | + } |
| 211 | + } |
| 212 | + |
| 213 | + function setupWatchers(field, index) { |
| 214 | + if (!angular.isDefined(field.watcher)) { |
| 215 | + return |
| 216 | + } |
| 217 | + let watchers = field.watcher |
| 218 | + if (!angular.isArray(watchers)) { |
| 219 | + watchers = [watchers] |
| 220 | + } |
| 221 | + angular.forEach(watchers, function setupWatcher(watcher) { |
| 222 | + if (!angular.isDefined(watcher.listener) && !watcher.runFieldExpressions) { |
| 223 | + throw formlyUsability.getFieldError( |
| 224 | + 'all-field-watchers-must-have-a-listener', |
| 225 | + 'All field watchers must have a listener', field |
| 226 | + ) |
| 227 | + } |
| 228 | + const watchExpression = getWatchExpression(watcher, field, index) |
| 229 | + const watchListener = getWatchListener(watcher, field, index) |
| 230 | + |
| 231 | + const type = watcher.type || '$watch' |
| 232 | + watcher.stopWatching = $scope[type](watchExpression, watchListener, watcher.watchDeep) |
| 233 | + }) |
| 234 | + } |
| 235 | + |
| 236 | + function getWatchExpression(watcher, field, index) { |
| 237 | + let watchExpression |
| 238 | + if (!angular.isUndefined(watcher.expression)) { |
| 239 | + watchExpression = watcher.expression |
| 240 | + } else if (field.key) { |
| 241 | + watchExpression = 'model[\'' + field.key.toString().split('.').join('\'][\'') + '\']' |
| 242 | + } |
| 243 | + if (angular.isFunction(watchExpression)) { |
| 244 | + // wrap the field's watch expression so we can call it with the field as the first arg |
| 245 | + // and the stop function as the last arg as a helper |
| 246 | + const originalExpression = watchExpression |
| 247 | + watchExpression = function formlyWatchExpression() { |
| 248 | + const args = modifyArgs(watcher, index, ...arguments) |
| 249 | + return originalExpression(...args) |
| 250 | + } |
| 251 | + watchExpression.displayName = `Formly Watch Expression for field for ${field.key}` |
| 252 | + } else if (field.model) { |
| 253 | + watchExpression = $parse(watchExpression).bind(null, $scope, {model: field.model}) |
| 254 | + } |
| 255 | + return watchExpression |
| 256 | + } |
| 257 | + |
| 258 | + function getWatchListener(watcher, field, index) { |
| 259 | + let watchListener = watcher.listener |
| 260 | + if (angular.isFunction(watchListener) || watcher.runFieldExpressions) { |
| 261 | + // wrap the field's watch listener so we can call it with the field as the first arg |
| 262 | + // and the stop function as the last arg as a helper |
| 263 | + const originalListener = watchListener |
| 264 | + watchListener = function formlyWatchListener() { |
| 265 | + let value |
| 266 | + if (originalListener) { |
| 267 | + const args = modifyArgs(watcher, index, ...arguments) |
| 268 | + value = originalListener(...args) |
| 269 | + } |
| 270 | + if (watcher.runFieldExpressions) { |
| 271 | + runFieldExpressionProperties(field, index) |
| 272 | + } |
| 273 | + return value |
| 274 | + } |
| 275 | + watchListener.displayName = `Formly Watch Listener for field for ${field.key}` |
| 276 | + } |
| 277 | + return watchListener |
| 278 | + } |
| 279 | + |
| 280 | + function modifyArgs(watcher, index, ...originalArgs) { |
| 281 | + return [$scope.fields[index], ...originalArgs, watcher.stopWatching] |
| 282 | + } |
| 283 | + |
| 284 | + function evalCloseToFormlyExpression(expression, val, field, index, extraLocals = {}) { |
| 285 | + extraLocals = angular.extend(getFormlyFieldLikeLocals(field, index), extraLocals) |
| 286 | + return formlyUtil.formlyEval($scope, expression, val, val, extraLocals) |
| 287 | + } |
| 288 | + |
| 289 | + function getFormlyFieldLikeLocals(field, index) { |
| 290 | + // this makes it closer to what a regular formlyExpression would be |
| 291 | + return { |
| 292 | + model: field.model, |
| 293 | + options: field, |
| 294 | + index, |
| 295 | + formState: $scope.options.formState, |
| 296 | + originalModel: $scope.model, |
| 297 | + formOptions: $scope.options, |
| 298 | + formId: $scope.formId, |
| 299 | + } |
| 300 | + } |
| 301 | +} |
0 commit comments