angular-formly
Version:
AngularJS directive which takes JSON representing a form and renders to HTML
351 lines (317 loc) • 12.5 kB
JavaScript
let angular = require('angular-fix');
module.exports = ngModule => {
ngModule.directive('formlyField', formlyField);
formlyField.tests = ON_TEST ? require('./formly-field.test')(ngModule) : null;
/**
* @ngdoc directive
* @name formlyField
* @restrict AE
*/
function formlyField($http, $q, $compile, $templateCache, formlyConfig, formlyValidationMessages, formlyApiCheck,
formlyUtil, formlyUsability, formlyWarn) {
return {
restrict: 'AE',
transclude: true,
scope: {
options: '=',
model: '=',
formId: '=?',
index: '=?',
fields: '=?',
formState: '=?',
form: '=?'
},
controller: function fieldController($scope, $timeout, $parse, $controller) {
var opts = $scope.options;
var fieldType = opts.type && formlyConfig.getType(opts.type);
simplifyLife(opts);
mergeFieldOptionsWithTypeDefaults(opts, fieldType);
extendOptionsWithDefaults(opts, $scope.index);
checkApi(opts);
// set field id to link labels and fields
$scope.id = formlyUtil.getFieldId($scope.formId, opts, $scope.index);
// initalization
runExpressions();
setFormControl($scope, opts);
addModelWatcher($scope, opts);
addValidationMessages(opts);
// simplify things
// create $scope.to so template authors can reference to instead of $scope.options.templateOptions
$scope.to = $scope.options.templateOptions;
invokeControllers($scope, opts, fieldType);
// function definitions
function runExpressions() {
$timeout(function() { // must run on next tick to make sure that the current value is correct.
var field = $scope.options;
var currentValue = valueGetterSetter();
angular.forEach(field.expressionProperties, function runExpression(expression, prop) {
var setter = $parse(prop).assign;
var promise = $q.when(formlyUtil.formlyEval($scope, expression, currentValue));
promise.then(function(value) {
setter(field, value);
});
});
});
}
function valueGetterSetter(newVal) {
if (!$scope.model || !$scope.options.key) {
return;
}
if (angular.isDefined(newVal)) {
$scope.model[$scope.options.key] = newVal;
}
return $scope.model[$scope.options.key];
}
function simplifyLife(options) {
// add a few empty objects (if they don't already exist) so you don't have to undefined check everywhere
formlyUtil.reverseDeepMerge(options, {
data: {},
templateOptions: {},
validation: {}
});
}
function mergeFieldOptionsWithTypeDefaults(options, type) {
if (type) {
mergeOptions(options, type.defaultOptions);
}
var properOrder = arrayify(options.optionsTypes).reverse(); // so the right things are overridden
angular.forEach(properOrder, typeName => {
mergeOptions(options, formlyConfig.getType(typeName, true, options).defaultOptions);
});
}
function mergeOptions(options, extraOptions) {
if (extraOptions) {
if (angular.isFunction(extraOptions)) {
extraOptions = extraOptions(options);
}
formlyUtil.reverseDeepMerge(options, extraOptions);
}
}
function extendOptionsWithDefaults(options, index) {
angular.extend(options, {
// attach the key in case the formly-field directive is used directly
key: options.key || index || 0,
value: valueGetterSetter,
runExpressions: runExpressions
});
}
// initialization functions
function setFormControl(scope, options) {
if (options.noFormControl) {
return;
}
scope.$watch('form["' + scope.id + '"]', function(formControl) {
if (formControl) {
scope.fc = formControl; // shortcut for template authors
scope.options.formControl = formControl;
addShowMessagesWatcher(scope, options);
}
});
}
function addModelWatcher(scope, options) {
if (options.model) {
scope.$watch('options.model', runExpressions, true);
}
}
function addShowMessagesWatcher(scope, options) {
scope.$watch(function() {
if (typeof scope.options.validation.show === 'boolean') {
return scope.fc.$invalid && scope.options.validation.show;
} else {
return scope.fc.$invalid && scope.fc.$touched;
}
}, function(show) {
options.validation.errorExistsAndShouldBeVisible = show;
scope.showError = show; // shortcut for template authors
});
}
function addValidationMessages(options) {
options.validation.messages = options.validation.messages || {};
angular.forEach(formlyValidationMessages.messages, function (expression, name) {
if (!options.validation.messages[name]) {
options.validation.messages[name] = function (viewValue, modelValue, scope) {
return formlyUtil.formlyEval(scope, expression, modelValue, viewValue);
};
}
});
}
function invokeControllers(scope, options = {}, type = {}) {
angular.forEach([type.controller, options.controller], controller => {
if (controller) {
$controller(controller, {$scope: scope});
}
});
}
},
link: function fieldLink(scope, el) {
var type = scope.options.type && formlyConfig.getType(scope.options.type);
var args = arguments;
var thusly = this;
getFieldTemplate(scope.options)
.then(runManipulators(formlyConfig.templateManipulators.preWrapper))
.then(transcludeInWrappers(scope.options))
.then(runManipulators(formlyConfig.templateManipulators.postWrapper))
.then(setElementTemplate)
.catch(error => {
formlyWarn(
'there-was-a-problem-setting-the-template-for-this-field',
'There was a problem setting the template for this field ',
scope.options,
error
);
});
function setElementTemplate(templateEl) {
el.html(asHtml(templateEl));
$compile(el.contents())(scope);
if (type && type.link) {
type.link.apply(thusly, args);
}
if (scope.options.link) {
scope.options.link.apply(thusly, args);
}
}
function runManipulators(manipulators) {
return function runManipulatorsOnTemplate(template) {
var chain = $q.when(template);
angular.forEach(manipulators, manipulator => {
chain = chain.then(template => {
return $q.when(manipulator(template, scope.options, scope)).then(newTemplate => {
return angular.isString(newTemplate) ? newTemplate : asHtml(newTemplate);
});
});
});
return chain;
};
}
}
};
function asHtml(el) {
var wrapper = angular.element('<a></a>');
return wrapper.append(el).html();
}
function getFieldTemplate(options) {
let type = formlyConfig.getType(options.type, true, options);
let template = options.template || type && type.template;
let templateUrl = options.templateUrl || type && type.templateUrl;
if (!template && !templateUrl) {
throw formlyUsability.getFieldError(
'type-type-has-no-template',
`Type '${options.type}' has not template. On element:`, options
);
}
return getTemplate(template || templateUrl, !template);
}
function getTemplate(template, isUrl) {
if (!isUrl) {
return $q.when(template);
} else {
let httpOptions = {cache: $templateCache};
return $http.get(template, httpOptions).then(function(response) {
return response.data;
}).catch(function(error) {
formlyWarn(
'problem-loading-template-for-templateurl',
'Problem loading template for ' + template,
error
);
});
}
}
function transcludeInWrappers(options) {
let wrapper = getWrapperOption(options);
return function transcludeTemplate(template) {
if (!wrapper.length) {
return $q.when(template);
}
wrapper.forEach((wrapper) => {
formlyUsability.checkWrapper(wrapper, options);
wrapper.validateOptions && wrapper.validateOptions(options);
runApiCheck(wrapper, options);
});
let promises = wrapper.map(w => getTemplate(w.template || w.templateUrl, !w.template));
return $q.all(promises).then(wrappersTemplates => {
wrappersTemplates.forEach((wrapperTemplate, index) => {
formlyUsability.checkWrapperTemplate(wrapperTemplate, wrapper[index]);
});
wrappersTemplates.reverse(); // wrapper 0 is wrapped in wrapper 1 and so on...
let totalWrapper = wrappersTemplates.shift();
wrappersTemplates.forEach(wrapperTemplate => {
totalWrapper = doTransclusion(totalWrapper, wrapperTemplate);
});
return doTransclusion(totalWrapper, template);
});
};
}
function doTransclusion(wrapper, template) {
let superWrapper = angular.element('<a></a>'); // this allows people not have to have a single root in wrappers
superWrapper.append(wrapper);
let transcludeEl = superWrapper.find('formly-transclude');
if (!transcludeEl.length) {
//try it using our custom find function
transcludeEl = formlyUtil.findByNodeName(superWrapper, 'formly-transclude');
}
transcludeEl.replaceWith(template);
return superWrapper.html();
}
function getWrapperOption(options) {
let wrapper = options.wrapper;
// explicit null means no wrapper
if (wrapper === null) {
return [];
}
// nothing specified means use the default wrapper for the type
if (!wrapper) {
// get all wrappers that specify they apply to this type
wrapper = arrayify(formlyConfig.getWrapperByType(options.type));
} else {
wrapper = arrayify(wrapper).map(formlyConfig.getWrapper);
}
// get all wrappers for that this type specified that it uses.
var type = formlyConfig.getType(options.type, true, options);
if (type && type.wrapper) {
let typeWrappers = arrayify(type.wrapper).map(formlyConfig.getWrapper);
wrapper = wrapper.concat(typeWrappers);
}
// add the default wrapper last
var defaultWrapper = formlyConfig.getWrapper();
if (defaultWrapper) {
wrapper.push(defaultWrapper);
}
return wrapper;
}
function checkApi(options) {
formlyApiCheck.throw(formlyApiCheck.formlyFieldOptions, arguments, {
prefix: 'formly-field directive',
url: 'formly-field-directive-validation-failed'
});
// validate with the type
const type = options.type && formlyConfig.getType(options.type);
if (type) {
if (type.validateOptions) {
type.validateOptions(options);
}
runApiCheck(type, options);
}
}
function runApiCheck({apiCheck, apiCheckInstance, apiCheckFunction, apiCheckOptions}, options) {
if (!apiCheck) {
return;
}
const instance = apiCheckInstance || formlyApiCheck;
const fn = apiCheckFunction || 'warn';
const shape = instance.shape(apiCheck);
instance[fn](shape, [options], apiCheckOptions || {
prefix: `formly-field ${name}`,
url: formlyApiCheck.config.output.docsBaseUrl + 'formly-field-type-apicheck-failed'
});
}
}
function arrayify(obj) {
if (obj && !angular.isArray(obj)) {
obj = [obj];
} else if (!obj) {
obj = [];
}
return obj;
}
};