'use strict'; /** * @ngdoc service * @name angular.service.$formFactory * * @description * Use `$formFactory` to create a new instance of a {@link guide/dev_guide.forms form} * controller or to find the nearest form instance for a given DOM element. * * The form instance is a collection of widgets, and is responsible for life cycle and validation * of widget. * * Keep in mind that both form and widget instances are {@link api/angular.scope scopes}. * * @param {Form=} parentForm The form which should be the parent form of the new form controller. * If none specified default to the `rootForm`. * @returns {Form} A new form instance. * * @example * * This example shows how one could write a widget which would enable data-binding on * `contenteditable` feature of HTML. *

HTML:

editorForm = {{editorForm}}
it('should enter invalid HTML', function() { expect(element('form[name=editorForm]').prop('className')).toMatch(/ng-valid/); input('html').enter('<'); expect(element('form[name=editorForm]').prop('className')).toMatch(/ng-invalid/); });
*/ angularServiceInject('$formFactory', function() { /** * @ngdoc proprety * @name rootForm * @propertyOf angular.service.$formFactory * @description * Static property on `$formFactory` * * Each application ({@link guide/dev_guide.scopes.internals root scope}) gets a root form which * is the top-level parent of all forms. */ formFactory.rootForm = formFactory(this); /** * @ngdoc method * @name forElement * @methodOf angular.service.$formFactory * @description * Static method on `$formFactory` service. * * Retrieve the closest form for a given element or defaults to the `root` form. Used by the * {@link angular.widget.form form} element. * @param {Element} element The element where the search for form should initiate. */ formFactory.forElement = function(element) { return element.inheritedData('$form') || formFactory.rootForm; }; return formFactory; function formFactory(parent) { return (parent || formFactory.rootForm).$new(FormController); } }); function propertiesUpdate(widget) { widget.$valid = !(widget.$invalid = !(widget.$readonly || widget.$disabled || equals(widget.$error, {}))); } /** * @ngdoc property * @name $error * @propertyOf angular.service.$formFactory * @description * Property of the form and widget instance. * * Summary of all of the errors on the page. If a widget emits `$invalid` with `REQUIRED` key, * then the `$error` object will have a `REQUIRED` key with an array of widgets which have * emitted this key. `form.$error.REQUIRED == [ widget ]`. */ /** * @workInProgress * @ngdoc property * @name $invalid * @propertyOf angular.service.$formFactory * @description * Property of the form and widget instance. * * True if any of the widgets of the form are invalid. */ /** * @workInProgress * @ngdoc property * @name $valid * @propertyOf angular.service.$formFactory * @description * Property of the form and widget instance. * * True if all of the widgets of the form are valid. */ /** * @ngdoc event * @name angular.service.$formFactory#$valid * @eventOf angular.service.$formFactory * @eventType listen on form * @description * Upon receiving the `$valid` event from the widget update the `$error`, `$valid` and `$invalid` * properties of both the widget as well as the from. * * @param {String} validationKey The validation key to be used when updating the `$error` object. * The validation key is what will allow the template to bind to a specific validation error * such as `
error for key
`. */ /** * @ngdoc event * @name angular.service.$formFactory#$invalid * @eventOf angular.service.$formFactory * @eventType listen on form * @description * Upon receiving the `$invalid` event from the widget update the `$error`, `$valid` and `$invalid` * properties of both the widget as well as the from. * * @param {String} validationKey The validation key to be used when updating the `$error` object. * The validation key is what will allow the template to bind to a specific validation error * such as `
error for key
`. */ /** * @ngdoc event * @name angular.service.$formFactory#$validate * @eventOf angular.service.$formFactory * @eventType emit on widget * @description * Emit the `$validate` event on the widget, giving a widget a chance to emit a * `$valid` / `$invalid` event base on its state. The `$validate` event is triggered when the * model or the view changes. */ /** * @ngdoc event * @name angular.service.$formFactory#$viewChange * @eventOf angular.service.$formFactory * @eventType listen on widget * @description * A widget is responsible for emitting this event whenever the view changes do to user interaction. * The event takes a `$viewValue` parameter, which is the new value of the view. This * event triggers a call to `$parseView()` as well as `$validate` event on widget. * * @param {*} viewValue The new value for the view which will be assigned to `widget.$viewValue`. */ function FormController() { var form = this, $error = form.$error = {}; form.$on('$destroy', function(event){ var widget = event.targetScope; if (widget.$widgetId) { delete form[widget.$widgetId]; } forEach($error, removeWidget, widget); }); form.$on('$valid', function(event, error){ var widget = event.targetScope; delete widget.$error[error]; propertiesUpdate(widget); removeWidget($error[error], error, widget); }); form.$on('$invalid', function(event, error){ var widget = event.targetScope; addWidget(error, widget); widget.$error[error] = true; propertiesUpdate(widget); }); propertiesUpdate(form); function removeWidget(queue, errorKey, widget) { if (queue) { widget = widget || this; // so that we can be used in forEach; for (var i = 0, length = queue.length; i < length; i++) { if (queue[i] === widget) { queue.splice(i, 1); if (!queue.length) { delete $error[errorKey]; } } } propertiesUpdate(form); } } function addWidget(errorKey, widget) { var queue = $error[errorKey]; if (queue) { for (var i = 0, length = queue.length; i < length; i++) { if (queue[i] === widget) { return; } } } else { $error[errorKey] = queue = []; } queue.push(widget); propertiesUpdate(form); } } /** * @ngdoc method * @name $createWidget * @methodOf angular.service.$formFactory * @description * * Use form's `$createWidget` instance method to create new widgets. The widgets can be created * using an alias which makes the accessible from the form and available for data-binding, * useful for displaying validation error messages. * * The creation of a widget sets up: * * - `$watch` of `expression` on `model` scope. This code path syncs the model to the view. * The `$watch` listener will: * * - assign the new model value of `expression` to `widget.$modelValue`. * - call `widget.$parseModel` method if present. The `$parseModel` is responsible for copying * the `widget.$modelValue` to `widget.$viewValue` and optionally converting the data. * (For example to convert a number into string) * - emits `$validate` event on widget giving a widget a chance to emit `$valid` / `$invalid` * event. * - call `widget.$render()` method on widget. The `$render` method is responsible for * reading the `widget.$viewValue` and updating the DOM. * * - Listen on `$viewChange` event from the `widget`. This code path syncs the view to the model. * The `$viewChange` listener will: * * - assign the value to `widget.$viewValue`. * - call `widget.$parseView` method if present. The `$parseView` is responsible for copying * the `widget.$viewValue` to `widget.$modelValue` and optionally converting the data. * (For example to convert a string into number) * - emits `$validate` event on widget giving a widget a chance to emit `$valid` / `$invalid` * event. * - Assign the `widget.$modelValue` to the `expression` on the `model` scope. * * - Creates these set of properties on the `widget` which are updated as a response to the * `$valid` / `$invalid` events: * * - `$error` - object - validation errors will be published as keys on this object. * Data-binding to this property is useful for displaying the validation errors. * - `$valid` - boolean - true if there are no validation errors * - `$invalid` - boolean - opposite of `$valid`. * @param {Object} params Named parameters: * * - `scope` - `{Scope}` - The scope to which the model for this widget is attached. * - `model` - `{string}` - The name of the model property on model scope. * - `controller` - {WidgetController} - The controller constructor function. * The controller constructor should create these instance methods. * - `$parseView()`: optional method responsible for copying `$viewVale` to `$modelValue`. * The method may fire `$valid`/`$invalid` events. * - `$parseModel()`: optional method responsible for copying `$modelVale` to `$viewValue`. * The method may fire `$valid`/`$invalid` events. * - `$render()`: required method which needs to update the DOM of the widget to match the * `$viewValue`. * * - `controllerArgs` - `{Array}` (Optional) - Any extra arguments will be curried to the * WidgetController constructor. * - `onChange` - `{(string|function())}` (Optional) - Expression to execute when user changes the * value. * - `alias` - `{string}` (Optional) - The name of the form property under which the widget * instance should be published. The name should be unique for each form. * @returns {Widget} Instance of a widget scope. */ FormController.prototype.$createWidget = function(params) { var form = this, modelScope = params.scope, onChange = params.onChange, alias = params.alias, scopeGet = parser(params.model).assignable(), scopeSet = scopeGet.assign, widget = this.$new(params.controller, params.controllerArgs); widget.$error = {}; // Set the state to something we know will change to get the process going. widget.$modelValue = Number.NaN; // watch for scope changes and update the view appropriately modelScope.$watch(scopeGet, function(scope, value) { if (!equals(widget.$modelValue, value)) { widget.$modelValue = value; widget.$parseModel ? widget.$parseModel() : (widget.$viewValue = value); widget.$emit('$validate'); widget.$render && widget.$render(); } }); widget.$on('$viewChange', function(event, viewValue){ if (!equals(widget.$viewValue, viewValue)) { widget.$viewValue = viewValue; widget.$parseView ? widget.$parseView() : (widget.$modelValue = widget.$viewValue); scopeSet(modelScope, widget.$modelValue); if (onChange) modelScope.$eval(onChange); widget.$emit('$validate'); } }); propertiesUpdate(widget); // assign the widgetModel to the form if (alias && !form.hasOwnProperty(alias)) { form[alias] = widget; widget.$widgetId = alias; } else { alias = null; } return widget; };