mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-03-17 07:40:22 +00:00
So that we can allow user to override this service and use BC hack: https://gist.github.com/1649788
411 lines
15 KiB
JavaScript
411 lines
15 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* @ngdoc object
|
|
* @name angular.module.ng.$formFactory
|
|
*
|
|
* @description
|
|
* Use `$formFactory` to create a new instance of a {@link angular.module.ng.$formFactory.Form 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.module.ng.$rootScope.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 {@link angular.module.ng.$formFactory.Form Form} instance.
|
|
*
|
|
* @example
|
|
*
|
|
* This example shows how one could write a widget which would enable data-binding on
|
|
* `contenteditable` feature of HTML.
|
|
*
|
|
<doc:example>
|
|
<doc:source>
|
|
<script>
|
|
function EditorCntl($scope) {
|
|
$scope.html = '<b>Hello</b> <i>World</i>!';
|
|
}
|
|
|
|
HTMLEditorWidget.$inject = ['$element', '$scope', 'htmlFilter'];
|
|
function HTMLEditorWidget(element, scope, htmlFilter) {
|
|
scope.$parseModel = function() {
|
|
// need to protect for script injection
|
|
try {
|
|
this.$viewValue = htmlFilter(this.$modelValue || '').get();
|
|
if (this.$error.HTML) {
|
|
// we were invalid, but now we are OK.
|
|
this.$emit('$valid', 'HTML');
|
|
}
|
|
} catch (e) {
|
|
// if HTML not parsable invalidate form.
|
|
this.$emit('$invalid', 'HTML');
|
|
}
|
|
}
|
|
|
|
scope.$render = function() {
|
|
element.html(this.$viewValue);
|
|
}
|
|
|
|
element.bind('keyup', function() {
|
|
scope.$apply(function() {
|
|
scope.$emit('$viewChange', element.html());
|
|
});
|
|
});
|
|
}
|
|
|
|
angular.directive('ng:contenteditable', function() {
|
|
return ['$formFactory', '$element', function ($formFactory, element) {
|
|
var exp = element.attr('ng:contenteditable'),
|
|
form = $formFactory.forElement(element),
|
|
widget;
|
|
element.attr('contentEditable', true);
|
|
widget = form.$createWidget({
|
|
scope: this,
|
|
model: exp,
|
|
controller: HTMLEditorWidget,
|
|
controllerArgs: {$element: element}});
|
|
// if the element is destroyed, then we need to notify the form.
|
|
element.bind('$destroy', function() {
|
|
widget.$destroy();
|
|
});
|
|
}];
|
|
});
|
|
</script>
|
|
<form name='editorForm' ng:controller="EditorCntl">
|
|
<div ng:contenteditable="html"></div>
|
|
<hr/>
|
|
HTML: <br/>
|
|
<textarea ng:model="html" cols=80></textarea>
|
|
<hr/>
|
|
<pre>editorForm = {{editorForm}}</pre>
|
|
</form>
|
|
</doc:source>
|
|
<doc:scenario>
|
|
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/);
|
|
});
|
|
</doc:scenario>
|
|
</doc:example>
|
|
*/
|
|
|
|
/**
|
|
* @ngdoc object
|
|
* @name angular.module.ng.$formFactory.Form
|
|
* @description
|
|
* The `Form` is a controller which keeps track of the validity of the widgets contained within it.
|
|
*/
|
|
|
|
function $FormFactoryProvider() {
|
|
var $parse;
|
|
this.$get = ['$rootScope', '$parse', '$controller',
|
|
function($rootScope, $parse_, $controller) {
|
|
$parse = $parse_;
|
|
/**
|
|
* @ngdoc proprety
|
|
* @name rootForm
|
|
* @propertyOf angular.module.ng.$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($rootScope);
|
|
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name forElement
|
|
* @methodOf angular.module.ng.$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) {
|
|
var scope = (parent || formFactory.rootForm).$new();
|
|
$controller(FormController, scope);
|
|
return scope;
|
|
}
|
|
|
|
}];
|
|
|
|
function propertiesUpdate(widget) {
|
|
widget.$valid = !(widget.$invalid =
|
|
!(widget.$readonly || widget.$disabled || equals(widget.$error, {})));
|
|
}
|
|
|
|
/**
|
|
* @ngdoc property
|
|
* @name $error
|
|
* @propertyOf angular.module.ng.$formFactory.Form
|
|
* @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 ]`.
|
|
*/
|
|
|
|
/**
|
|
* @ngdoc property
|
|
* @name $invalid
|
|
* @propertyOf angular.module.ng.$formFactory.Form
|
|
* @description
|
|
* Property of the form and widget instance.
|
|
*
|
|
* True if any of the widgets of the form are invalid.
|
|
*/
|
|
|
|
/**
|
|
* @ngdoc property
|
|
* @name $valid
|
|
* @propertyOf angular.module.ng.$formFactory.Form
|
|
* @description
|
|
* Property of the form and widget instance.
|
|
*
|
|
* True if all of the widgets of the form are valid.
|
|
*/
|
|
|
|
/**
|
|
* @ngdoc event
|
|
* @name angular.module.ng.$formFactory.Form#$valid
|
|
* @eventOf angular.module.ng.$formFactory.Form
|
|
* @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 `<div ng:show="form.$error.KEY">error for key</div>`.
|
|
*/
|
|
|
|
/**
|
|
* @ngdoc event
|
|
* @name angular.module.ng.$formFactory.Form#$invalid
|
|
* @eventOf angular.module.ng.$formFactory.Form
|
|
* @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 `<div ng:show="form.$error.KEY">error for key</div>`.
|
|
*/
|
|
|
|
/**
|
|
* @ngdoc event
|
|
* @name angular.module.ng.$formFactory.Form#$validate
|
|
* @eventOf angular.module.ng.$formFactory.Form
|
|
* @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.module.ng.$formFactory.Form#$viewChange
|
|
* @eventOf angular.module.ng.$formFactory.Form
|
|
* @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`.
|
|
*/
|
|
|
|
FormController.$inject = ['$scope', '$injector'];
|
|
function FormController($scope, $injector) {
|
|
this.$injector = $injector;
|
|
|
|
var form = this.form = $scope,
|
|
$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);
|
|
form.$createWidget = bind(this, this.$createWidget);
|
|
|
|
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.module.ng.$formFactory.Form
|
|
* @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.form,
|
|
modelScope = params.scope,
|
|
onChange = params.onChange,
|
|
alias = params.alias,
|
|
scopeGet = $parse(params.model),
|
|
scopeSet = scopeGet.assign,
|
|
widget = form.$new();
|
|
|
|
this.$injector.instantiate(params.controller, extend({$scope: widget}, params.controllerArgs));
|
|
|
|
if (!scopeSet) {
|
|
throw Error("Expression '" + params.model + "' is not assignable!");
|
|
}
|
|
|
|
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(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;
|
|
};
|
|
}
|