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

Commit fe96b2b

Browse files
committed
feat(FormlyFormController): Exposes FormlyFormController globally
For the formly plugin I am writing, I want to reuse the logic in the FormlyFormController with a different directive. This change makes the controller available to third party code via the $controller service.
1 parent 21e96f1 commit fe96b2b

File tree

6 files changed

+428
-401
lines changed

6 files changed

+428
-401
lines changed
Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
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

Comments
 (0)