mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-03-17 07:40:22 +00:00
refactor(forms): Even better forms
- remove $formFactory completely - remove parallel scope hierarchy (forms, widgets) - use new compiler features (widgets, forms are controllers) - any directive can add formatter/parser (validators, convertors) Breaks no custom input types Breaks removed integer input type Breaks remove list input type (ng-list directive instead) Breaks inputs bind only blur event by default (added ng:bind-change directive)
This commit is contained in:
parent
e23fa768aa
commit
21c725f1a1
18 changed files with 2330 additions and 2206 deletions
1
angularFiles.js
vendored
1
angularFiles.js
vendored
|
|
@ -23,7 +23,6 @@ angularFiles = {
|
|||
'src/service/filter/filters.js',
|
||||
'src/service/filter/limitTo.js',
|
||||
'src/service/filter/orderBy.js',
|
||||
'src/service/formFactory.js',
|
||||
'src/service/interpolate.js',
|
||||
'src/service/location.js',
|
||||
'src/service/log.js',
|
||||
|
|
|
|||
|
|
@ -32,61 +32,3 @@ All `inputType` widgets support:
|
|||
- **`ng:pattern`** Sets `PATTERN` validation error key if the value does not match the
|
||||
RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
|
||||
patterns defined as scope expressions.
|
||||
|
||||
|
||||
|
||||
# Example
|
||||
|
||||
<doc:example>
|
||||
<doc:source>
|
||||
<script>
|
||||
angular.inputType('json', function(element, scope) {
|
||||
scope.$parseView = function() {
|
||||
try {
|
||||
this.$modelValue = angular.fromJson(this.$viewValue);
|
||||
if (this.$error.JSON) {
|
||||
this.$emit('$valid', 'JSON');
|
||||
}
|
||||
} catch (e) {
|
||||
this.$emit('$invalid', 'JSON');
|
||||
}
|
||||
}
|
||||
|
||||
scope.$parseModel = function() {
|
||||
this.$viewValue = angular.toJson(this.$modelValue);
|
||||
}
|
||||
});
|
||||
|
||||
function Ctrl($scope) {
|
||||
$scope.data = {
|
||||
framework:'angular',
|
||||
codenames:'supper-powers'
|
||||
}
|
||||
$scope.required = false;
|
||||
$scope.disabled = false;
|
||||
$scope.readonly = false;
|
||||
}
|
||||
</script>
|
||||
<div ng:controller="Ctrl">
|
||||
<form name="myForm">
|
||||
<input type="json" ng:model="data" size="80"
|
||||
ng:required="{{required}}" ng:disabled="{{disabled}}"
|
||||
ng:readonly="{{readonly}}"/><br/>
|
||||
Required: <input type="checkbox" ng:model="required"> <br/>
|
||||
Disabled: <input type="checkbox" ng:model="disabled"> <br/>
|
||||
Readonly: <input type="checkbox" ng:model="readonly"> <br/>
|
||||
<pre>data={{data}}</pre>
|
||||
<pre>myForm={{myForm}}</pre>
|
||||
</form>
|
||||
</div>
|
||||
</doc:source>
|
||||
<doc:scenario>
|
||||
it('should invalidate on wrong input', function() {
|
||||
expect(element('form[name=myForm]').prop('className')).toMatch('ng-valid');
|
||||
input('data').enter('{}');
|
||||
expect(binding('data')).toEqual('{}');
|
||||
input('data').enter('{');
|
||||
expect(element('form[name=myForm]').prop('className')).toMatch('ng-invalid');
|
||||
});
|
||||
</doc:scenario>
|
||||
</doc:example>
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ detection, and preventing invalid form submission.
|
|||
};
|
||||
|
||||
$scope.isSaveDisabled = function() {
|
||||
return $scope.myForm.$invalid || angular.equals(master, $scope.form);
|
||||
return $scope.myForm.invalid || angular.equals(master, $scope.form);
|
||||
};
|
||||
|
||||
$scope.cancel();
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ The following example demonstrates:
|
|||
};
|
||||
|
||||
$scope.isSaveDisabled = function() {
|
||||
return $scope.userForm.$invalid || angular.equals($scope.master, $scope.form);
|
||||
return $scope.userForm.invalid || angular.equals($scope.master, $scope.form);
|
||||
};
|
||||
|
||||
$scope.cancel();
|
||||
|
|
@ -150,7 +150,7 @@ The following example demonstrates:
|
|||
|
||||
<label>Name:</label><br/>
|
||||
<input type="text" name="customer" ng:model="form.customer" required/>
|
||||
<span class="error" ng:show="userForm.customer.$error.REQUIRED">
|
||||
<span class="error" ng:show="userForm.customer.error.REQUIRED">
|
||||
Customer name is required!</span>
|
||||
<br/><br/>
|
||||
|
||||
|
|
@ -165,15 +165,15 @@ The following example demonstrates:
|
|||
<input type="text" name="zip" ng:pattern="zip" size="5" required
|
||||
ng:model="form.address.zip"/><br/><br/>
|
||||
|
||||
<span class="error" ng:show="addressForm.$invalid">
|
||||
<span class="error" ng:show="addressForm.invalid">
|
||||
Incomplete address:
|
||||
<span class="error" ng:show="addressForm.state.$error.REQUIRED">
|
||||
<span class="error" ng:show="addressForm.state.error.REQUIRED">
|
||||
Missing state!</span>
|
||||
<span class="error" ng:show="addressForm.state.$error.PATTERN">
|
||||
<span class="error" ng:show="addressForm.state.error.PATTERN">
|
||||
Invalid state!</span>
|
||||
<span class="error" ng:show="addressForm.zip.$error.REQUIRED">
|
||||
<span class="error" ng:show="addressForm.zip.error.REQUIRED">
|
||||
Missing zip!</span>
|
||||
<span class="error" ng:show="addressForm.zip.$error.PATTERN">
|
||||
<span class="error" ng:show="addressForm.zip.error.PATTERN">
|
||||
Invalid zip!</span>
|
||||
</span>
|
||||
</ng:form>
|
||||
|
|
@ -284,56 +284,38 @@ This example shows how to implement a custom HTML editor widget in Angular.
|
|||
$scope.htmlContent = '<b>Hello</b> <i>World</i>!';
|
||||
}
|
||||
|
||||
HTMLEditorWidget.$inject = ['$scope', '$element', '$sanitize'];
|
||||
function HTMLEditorWidget(scope, element, $sanitize) {
|
||||
scope.$parseModel = function() {
|
||||
// need to protect for script injection
|
||||
try {
|
||||
scope.$viewValue = $sanitize(
|
||||
scope.$modelValue || '');
|
||||
if (this.$error.HTML) {
|
||||
// we were invalid, but now we are OK.
|
||||
scope.$emit('$valid', 'HTML');
|
||||
}
|
||||
} catch (e) {
|
||||
// if HTML not parsable invalidate form.
|
||||
scope.$emit('$invalid', 'HTML');
|
||||
}
|
||||
}
|
||||
angular.module('formModule', []).directive('ngHtmlEditor', function ($sanitize) {
|
||||
return {
|
||||
require: 'ngModel',
|
||||
link: function(scope, elm, attr, ctrl) {
|
||||
attr.$set('contentEditable', true);
|
||||
|
||||
scope.$render = function() {
|
||||
element.html(this.$viewValue);
|
||||
}
|
||||
ctrl.$render = function() {
|
||||
elm.html(ctrl.viewValue);
|
||||
};
|
||||
|
||||
element.bind('keyup', function() {
|
||||
scope.$apply(function() {
|
||||
scope.$emit('$viewChange', element.html());
|
||||
});
|
||||
});
|
||||
}
|
||||
ctrl.formatters.push(function(value) {
|
||||
try {
|
||||
value = $sanitize(value || '');
|
||||
ctrl.emitValidity('HTML', true);
|
||||
} catch (e) {
|
||||
ctrl.emitValidity('HTML', false);
|
||||
}
|
||||
|
||||
angular.module('formModule', [], function($compileProvider){
|
||||
$compileProvider.directive('ngHtmlEditorModel', function ($formFactory) {
|
||||
return function(scope, element, attr) {
|
||||
var form = $formFactory.forElement(element),
|
||||
widget;
|
||||
element.attr('contentEditable', true);
|
||||
widget = form.$createWidget({
|
||||
scope: scope,
|
||||
model: attr.ngHtmlEditorModel,
|
||||
controller: HTMLEditorWidget,
|
||||
controllerArgs: {$element: element}});
|
||||
// if the element is destroyed, then we need to
|
||||
// notify the form.
|
||||
element.bind('$destroy', function() {
|
||||
widget.$destroy();
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
elm.bind('keyup', function() {
|
||||
scope.$apply(function() {
|
||||
ctrl.read(elm.html());
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
});
|
||||
</script>
|
||||
<form name='editorForm' ng:controller="EditorCntl">
|
||||
<div ng:html-editor-model="htmlContent"></div>
|
||||
<div ng:html-editor ng:model="htmlContent"></div>
|
||||
<hr/>
|
||||
HTML: <br/>
|
||||
<textarea ng:model="htmlContent" cols="80"></textarea>
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@
|
|||
|
||||
<div id="sidebar">
|
||||
<input type="text" ng:model="search" id="search-box" placeholder="search the docs"
|
||||
tabindex="1" accesskey="s">
|
||||
tabindex="1" accesskey="s" ng:bind-immediate>
|
||||
|
||||
<ul id="content-list" ng:class="sectionId" ng:cloak>
|
||||
<li ng:repeat="page in pages | filter:search" ng:class="getClass(page)">
|
||||
|
|
|
|||
|
|
@ -91,7 +91,6 @@ var $boolean = 'boolean',
|
|||
angular = window.angular || (window.angular = {}),
|
||||
angularModule,
|
||||
/** @name angular.module.ng */
|
||||
angularInputType = extensionMap(angular, 'inputType', lowercase),
|
||||
nodeName_,
|
||||
uid = ['0', '0', '0'],
|
||||
DATE_ISOSTRING_LN = 24;
|
||||
|
|
@ -272,17 +271,6 @@ identity.$inject = [];
|
|||
|
||||
function valueFn(value) {return function() {return value;};}
|
||||
|
||||
function extensionMap(angular, name, transform) {
|
||||
var extPoint;
|
||||
return angular[name] || (extPoint = angular[name] = function(name, fn, prop){
|
||||
name = (transform || identity)(name);
|
||||
if (isDefined(fn)) {
|
||||
extPoint[name] = extend(fn, prop || {});
|
||||
}
|
||||
return extPoint[name];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @ngdoc function
|
||||
* @name angular.isUndefined
|
||||
|
|
|
|||
|
|
@ -98,7 +98,13 @@ function publishExternalAPI(angular){
|
|||
ngSwitchDefault: ngSwitchDefaultDirective,
|
||||
ngOptions: ngOptionsDirective,
|
||||
ngView: ngViewDirective,
|
||||
ngTransclude: ngTranscludeDirective
|
||||
ngTransclude: ngTranscludeDirective,
|
||||
ngModel: ngModelDirective,
|
||||
ngList: ngListDirective,
|
||||
ngChange: ngChangeDirective,
|
||||
ngBindImmediate: ngBindImmediateDirective,
|
||||
required: requiredDirective,
|
||||
ngRequired: requiredDirective
|
||||
}).
|
||||
directive(ngEventDirectives).
|
||||
directive(ngAttributeAliasDirectives);
|
||||
|
|
@ -110,7 +116,6 @@ function publishExternalAPI(angular){
|
|||
$provide.service('$exceptionHandler', $ExceptionHandlerProvider);
|
||||
$provide.service('$filter', $FilterProvider);
|
||||
$provide.service('$interpolate', $InterpolateProvider);
|
||||
$provide.service('$formFactory', $FormFactoryProvider);
|
||||
$provide.service('$http', $HttpProvider);
|
||||
$provide.service('$httpBackend', $HttpBackendProvider);
|
||||
$provide.service('$location', $LocationProvider);
|
||||
|
|
|
|||
|
|
@ -327,7 +327,7 @@ function browserTrigger(element, type, keys) {
|
|||
(function(fn){
|
||||
var parentTrigger = fn.trigger;
|
||||
fn.trigger = function(type) {
|
||||
if (/(click|change|keydown)/.test(type)) {
|
||||
if (/(click|change|keydown|blur)/.test(type)) {
|
||||
var processDefaults = [];
|
||||
this.each(function(index, node) {
|
||||
processDefaults.push(browserTrigger(node, type));
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ angular.scenario.dsl('input', function() {
|
|||
return this.addFutureAction("input '" + this.name + "' enter '" + value + "'", function($window, $document, done) {
|
||||
var input = $document.elements('[ng\\:model="$1"]', this.name).filter(':input');
|
||||
input.val(value);
|
||||
input.trigger('keydown');
|
||||
input.trigger('blur');
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,414 +0,0 @@
|
|||
'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 module="formModule">
|
||||
<doc:source>
|
||||
<script>
|
||||
function EditorCntl($scope) {
|
||||
$scope.htmlContent = '<b>Hello</b> <i>World</i>!';
|
||||
}
|
||||
|
||||
HTMLEditorWidget.$inject = ['$scope', '$element', '$sanitize'];
|
||||
function HTMLEditorWidget(scope, element, $sanitize) {
|
||||
scope.$parseModel = function() {
|
||||
// need to protect for script injection
|
||||
try {
|
||||
scope.$viewValue = $sanitize(
|
||||
scope.$modelValue || '');
|
||||
if (this.$error.HTML) {
|
||||
// we were invalid, but now we are OK.
|
||||
scope.$emit('$valid', 'HTML');
|
||||
}
|
||||
} catch (e) {
|
||||
// if HTML not parsable invalidate form.
|
||||
scope.$emit('$invalid', 'HTML');
|
||||
}
|
||||
}
|
||||
|
||||
scope.$render = function() {
|
||||
element.html(this.$viewValue);
|
||||
}
|
||||
|
||||
element.bind('keyup', function() {
|
||||
scope.$apply(function() {
|
||||
scope.$emit('$viewChange', element.html());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
angular.module('formModule', [], function($compileProvider){
|
||||
$compileProvider.directive('ngHtmlEditorModel', function ($formFactory) {
|
||||
return function(scope, element, attr) {
|
||||
var form = $formFactory.forElement(element),
|
||||
widget;
|
||||
element.attr('contentEditable', true);
|
||||
widget = form.$createWidget({
|
||||
scope: scope,
|
||||
model: attr.ngHtmlEditorModel,
|
||||
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:html-editor-model="htmlContent"></div>
|
||||
<hr/>
|
||||
HTML: <br/>
|
||||
<textarea ng:model="htmlContent" cols="80"></textarea>
|
||||
<hr/>
|
||||
<pre>editorForm = {{editorForm|json}}</pre>
|
||||
</form>
|
||||
</doc:source>
|
||||
<doc:scenario>
|
||||
it('should enter invalid HTML', function() {
|
||||
expect(element('form[name=editorForm]').prop('className')).toMatch(/ng-valid/);
|
||||
input('htmlContent').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.module.ng.$compileProvider.directive.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: 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;
|
||||
};
|
||||
}
|
||||
|
|
@ -1,5 +1,86 @@
|
|||
'use strict';
|
||||
|
||||
FormController.$inject = ['$scope', 'name'];
|
||||
function FormController($scope, name) {
|
||||
var form = this,
|
||||
errors = form.error = {};
|
||||
|
||||
// publish the form into scope
|
||||
name(this);
|
||||
|
||||
$scope.$on('$destroy', function(event, widget) {
|
||||
if (!widget) return;
|
||||
|
||||
if (widget.widgetId) {
|
||||
delete form[widget.widgetId];
|
||||
}
|
||||
forEach(errors, removeWidget, widget);
|
||||
});
|
||||
|
||||
$scope.$on('$valid', function(event, error, widget) {
|
||||
removeWidget(errors[error], error, widget);
|
||||
|
||||
if (equals(errors, {})) {
|
||||
form.valid = true;
|
||||
form.invalid = false;
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$on('$invalid', function(event, error, widget) {
|
||||
addWidget(error, widget);
|
||||
|
||||
form.valid = false;
|
||||
form.invalid = true;
|
||||
});
|
||||
|
||||
$scope.$on('$viewTouch', function() {
|
||||
form.dirty = true;
|
||||
form.pristine = false;
|
||||
});
|
||||
|
||||
// init state
|
||||
form.dirty = false;
|
||||
form.pristine = true;
|
||||
form.valid = true;
|
||||
form.invalid = false;
|
||||
|
||||
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 errors[errorKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addWidget(errorKey, widget) {
|
||||
var queue = errors[errorKey];
|
||||
if (queue) {
|
||||
for (var i = 0, length = queue.length; i < length; i++) {
|
||||
if (queue[i] === widget) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
errors[errorKey] = queue = [];
|
||||
}
|
||||
queue.push(widget);
|
||||
}
|
||||
}
|
||||
|
||||
FormController.prototype.registerWidget = function(widget, alias) {
|
||||
if (alias && !this.hasOwnProperty(alias)) {
|
||||
widget.widgetId = alias;
|
||||
this[alias] = widget;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @ngdoc directive
|
||||
* @name angular.module.ng.$compileProvider.directive.form
|
||||
|
|
@ -57,55 +138,54 @@
|
|||
$scope.text = 'guest';
|
||||
}
|
||||
</script>
|
||||
<div ng:controller="Ctrl">
|
||||
<form name="myForm">
|
||||
text: <input type="text" name="input" ng:model="text" required>
|
||||
<span class="error" ng:show="myForm.text.$error.REQUIRED">Required!</span>
|
||||
</form>
|
||||
<form name="myForm" ng:controller="Ctrl">
|
||||
text: <input type="text" name="input" ng:model="text" required>
|
||||
<span class="error" ng:show="myForm.input.error.REQUIRED">Required!</span>
|
||||
<tt>text = {{text}}</tt><br/>
|
||||
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
|
||||
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
|
||||
<tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
|
||||
<tt>myForm.$error.REQUIRED = {{!!myForm.$error.REQUIRED}}</tt><br/>
|
||||
</div>
|
||||
<tt>myForm.input.valid = {{myForm.input.valid}}</tt><br/>
|
||||
<tt>myForm.input.error = {{myForm.input.error}}</tt><br/>
|
||||
<tt>myForm.valid = {{myForm.valid}}</tt><br/>
|
||||
<tt>myForm.error.REQUIRED = {{!!myForm.error.REQUIRED}}</tt><br/>
|
||||
</form>
|
||||
</doc:source>
|
||||
<doc:scenario>
|
||||
it('should initialize to model', function() {
|
||||
expect(binding('text')).toEqual('guest');
|
||||
expect(binding('myForm.input.$valid')).toEqual('true');
|
||||
expect(binding('myForm.input.valid')).toEqual('true');
|
||||
});
|
||||
|
||||
it('should be invalid if empty', function() {
|
||||
input('text').enter('');
|
||||
expect(binding('text')).toEqual('');
|
||||
expect(binding('myForm.input.$valid')).toEqual('false');
|
||||
expect(binding('myForm.input.valid')).toEqual('false');
|
||||
});
|
||||
</doc:scenario>
|
||||
</doc:example>
|
||||
*/
|
||||
var ngFormDirective = ['$formFactory', function($formFactory) {
|
||||
var ngFormDirective = [function() {
|
||||
return {
|
||||
name: 'form',
|
||||
restrict: 'E',
|
||||
scope: true,
|
||||
inject: {
|
||||
name: 'accessor'
|
||||
},
|
||||
controller: FormController,
|
||||
compile: function() {
|
||||
return {
|
||||
pre: function(scope, formElement, attr) {
|
||||
var name = attr.name,
|
||||
parentForm = $formFactory.forElement(formElement),
|
||||
form = $formFactory(parentForm);
|
||||
formElement.data('$form', form);
|
||||
formElement.bind('submit', function(event){
|
||||
pre: function(scope, formElement, attr, controller) {
|
||||
formElement.data('$form', controller);
|
||||
formElement.bind('submit', function(event) {
|
||||
if (!attr.action) event.preventDefault();
|
||||
});
|
||||
if (name) {
|
||||
scope[name] = form;
|
||||
}
|
||||
watch('valid');
|
||||
watch('invalid');
|
||||
function watch(name) {
|
||||
form.$watch('$' + name, function(value) {
|
||||
|
||||
forEach(['valid', 'invalid', 'dirty', 'pristine'], function(name) {
|
||||
scope.$watch(function() {
|
||||
return controller[name];
|
||||
}, function(value) {
|
||||
formElement[value ? 'addClass' : 'removeClass']('ng-' + name);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
1007
src/widget/input.js
1007
src/widget/input.js
File diff suppressed because it is too large
Load diff
|
|
@ -123,87 +123,79 @@
|
|||
*/
|
||||
|
||||
var ngOptionsDirective = valueFn({ terminal: true });
|
||||
var selectDirective = ['$formFactory', '$compile', '$parse',
|
||||
function($formFactory, $compile, $parse){
|
||||
var selectDirective = ['$compile', '$parse', function($compile, $parse) {
|
||||
//00001111100000000000222200000000000000000000003333000000000000044444444444444444000000000555555555555555550000000666666666666666660000000000000007777
|
||||
var NG_OPTIONS_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?(?:\s+group\s+by\s+(.*))?\s+for\s+(?:([\$\w][\$\w\d]*)|(?:\(\s*([\$\w][\$\w\d]*)\s*,\s*([\$\w][\$\w\d]*)\s*\)))\s+in\s+(.*)$/;
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
link: function(modelScope, selectElement, attr) {
|
||||
if (!attr.ngModel) return;
|
||||
var form = $formFactory.forElement(selectElement),
|
||||
multiple = attr.multiple,
|
||||
optionsExp = attr.ngOptions,
|
||||
modelExp = attr.ngModel,
|
||||
widget = form.$createWidget({
|
||||
scope: modelScope,
|
||||
model: modelExp,
|
||||
onChange: attr.ngChange,
|
||||
alias: attr.name,
|
||||
controller: ['$scope', optionsExp ? Options : (multiple ? Multiple : Single)]});
|
||||
require: '?ngModel',
|
||||
link: function(scope, element, attr, ctrl) {
|
||||
if (!ctrl) return;
|
||||
|
||||
selectElement.bind('$destroy', function() { widget.$destroy(); });
|
||||
var multiple = attr.multiple,
|
||||
optionsExp = attr.ngOptions;
|
||||
|
||||
widget.$pristine = !(widget.$dirty = false);
|
||||
// required validator
|
||||
if (multiple && (attr.required || attr.ngRequired)) {
|
||||
var requiredValidator = function(value) {
|
||||
ctrl.emitValidity('REQUIRED', !attr.required || (value && value.length));
|
||||
return value;
|
||||
};
|
||||
|
||||
widget.$on('$validate', function() {
|
||||
var valid = !attr.required || !!widget.$modelValue;
|
||||
if (valid && multiple && attr.required) valid = !!widget.$modelValue.length;
|
||||
if (valid !== !widget.$error.REQUIRED) {
|
||||
widget.$emit(valid ? '$valid' : '$invalid', 'REQUIRED');
|
||||
}
|
||||
});
|
||||
ctrl.parsers.push(requiredValidator);
|
||||
ctrl.formatters.unshift(requiredValidator);
|
||||
|
||||
widget.$on('$viewChange', function() {
|
||||
widget.$pristine = !(widget.$dirty = true);
|
||||
});
|
||||
|
||||
forEach(['valid', 'invalid', 'pristine', 'dirty'], function(name) {
|
||||
widget.$watch('$' + name, function(value) {
|
||||
selectElement[value ? 'addClass' : 'removeClass']('ng-' + name);
|
||||
attr.$observe('required', function() {
|
||||
requiredValidator(ctrl.viewValue);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (optionsExp) Options(scope, element, ctrl);
|
||||
else if (multiple) Multiple(scope, element, ctrl);
|
||||
else Single(scope, element, ctrl);
|
||||
|
||||
|
||||
////////////////////////////
|
||||
|
||||
function Multiple(widget) {
|
||||
widget.$render = function() {
|
||||
var items = new HashMap(this.$viewValue);
|
||||
forEach(selectElement.children(), function(option){
|
||||
|
||||
|
||||
function Single(scope, selectElement, ctrl) {
|
||||
ctrl.render = function() {
|
||||
selectElement.val(ctrl.viewValue);
|
||||
};
|
||||
|
||||
selectElement.bind('change', function() {
|
||||
scope.$apply(function() {
|
||||
ctrl.touch();
|
||||
ctrl.read(selectElement.val());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function Multiple(scope, selectElement, ctrl) {
|
||||
ctrl.render = function() {
|
||||
var items = new HashMap(ctrl.viewValue);
|
||||
forEach(selectElement.children(), function(option) {
|
||||
option.selected = isDefined(items.get(option.value));
|
||||
});
|
||||
};
|
||||
|
||||
selectElement.bind('change', function() {
|
||||
widget.$apply(function() {
|
||||
scope.$apply(function() {
|
||||
var array = [];
|
||||
forEach(selectElement.children(), function(option){
|
||||
forEach(selectElement.children(), function(option) {
|
||||
if (option.selected) {
|
||||
array.push(option.value);
|
||||
}
|
||||
});
|
||||
widget.$emit('$viewChange', array);
|
||||
ctrl.touch();
|
||||
ctrl.read(array);
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function Single(widget) {
|
||||
widget.$render = function() {
|
||||
selectElement.val(widget.$viewValue);
|
||||
};
|
||||
|
||||
selectElement.bind('change', function() {
|
||||
widget.$apply(function() {
|
||||
widget.$emit('$viewChange', selectElement.val());
|
||||
});
|
||||
});
|
||||
|
||||
widget.$viewValue = selectElement.val();
|
||||
}
|
||||
|
||||
function Options(widget) {
|
||||
function Options(scope, selectElement, ctrl) {
|
||||
var match;
|
||||
|
||||
if (! (match = optionsExp.match(NG_OPTIONS_REGEXP))) {
|
||||
|
|
@ -234,15 +226,15 @@ var selectDirective = ['$formFactory', '$compile', '$parse',
|
|||
// developer declared null option, so user should be able to select it
|
||||
nullOption = jqLite(option).remove();
|
||||
// compile the element since there might be bindings in it
|
||||
$compile(nullOption)(modelScope);
|
||||
$compile(nullOption)(scope);
|
||||
}
|
||||
});
|
||||
selectElement.html(''); // clear contents
|
||||
|
||||
selectElement.bind('change', function() {
|
||||
widget.$apply(function() {
|
||||
scope.$apply(function() {
|
||||
var optionGroup,
|
||||
collection = valuesFn(modelScope) || [],
|
||||
collection = valuesFn(scope) || [],
|
||||
locals = {},
|
||||
key, value, optionElement, index, groupIndex, length, groupLength;
|
||||
|
||||
|
|
@ -259,7 +251,7 @@ var selectDirective = ['$formFactory', '$compile', '$parse',
|
|||
key = optionElement.val();
|
||||
if (keyName) locals[keyName] = key;
|
||||
locals[valueName] = collection[key];
|
||||
value.push(valueFn(modelScope, locals));
|
||||
value.push(valueFn(scope, locals));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -272,17 +264,21 @@ var selectDirective = ['$formFactory', '$compile', '$parse',
|
|||
} else {
|
||||
locals[valueName] = collection[key];
|
||||
if (keyName) locals[keyName] = key;
|
||||
value = valueFn(modelScope, locals);
|
||||
value = valueFn(scope, locals);
|
||||
}
|
||||
}
|
||||
if (isDefined(value) && modelScope.$viewVal !== value) {
|
||||
widget.$emit('$viewChange', value);
|
||||
ctrl.touch();
|
||||
|
||||
if (ctrl.viewValue !== value) {
|
||||
ctrl.read(value);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
widget.$watch(render);
|
||||
widget.$render = render;
|
||||
ctrl.render = render;
|
||||
|
||||
// TODO(vojta): can't we optimize this ?
|
||||
scope.$watch(render);
|
||||
|
||||
function render() {
|
||||
var optionGroups = {'':[]}, // Temporary location for the option groups before we render them
|
||||
|
|
@ -291,8 +287,8 @@ var selectDirective = ['$formFactory', '$compile', '$parse',
|
|||
optionGroup,
|
||||
option,
|
||||
existingParent, existingOptions, existingOption,
|
||||
modelValue = widget.$modelValue,
|
||||
values = valuesFn(modelScope) || [],
|
||||
modelValue = ctrl.modelValue,
|
||||
values = valuesFn(scope) || [],
|
||||
keys = keyName ? sortedKeys(values) : values,
|
||||
groupLength, length,
|
||||
groupIndex, index,
|
||||
|
|
@ -313,20 +309,20 @@ var selectDirective = ['$formFactory', '$compile', '$parse',
|
|||
// We now build up the list of options we need (we merge later)
|
||||
for (index = 0; length = keys.length, index < length; index++) {
|
||||
locals[valueName] = values[keyName ? locals[keyName]=keys[index]:index];
|
||||
optionGroupName = groupByFn(modelScope, locals) || '';
|
||||
optionGroupName = groupByFn(scope, locals) || '';
|
||||
if (!(optionGroup = optionGroups[optionGroupName])) {
|
||||
optionGroup = optionGroups[optionGroupName] = [];
|
||||
optionGroupNames.push(optionGroupName);
|
||||
}
|
||||
if (multiple) {
|
||||
selected = selectedSet.remove(valueFn(modelScope, locals)) != undefined;
|
||||
selected = selectedSet.remove(valueFn(scope, locals)) != undefined;
|
||||
} else {
|
||||
selected = modelValue === valueFn(modelScope, locals);
|
||||
selected = modelValue === valueFn(scope, locals);
|
||||
selectedSet = selectedSet || selected; // see if at least one item is selected
|
||||
}
|
||||
optionGroup.push({
|
||||
id: keyName ? keys[index] : index, // either the index into array or key from object
|
||||
label: displayFn(modelScope, locals) || '', // what will be seen by the user
|
||||
label: displayFn(scope, locals) || '', // what will be seen by the user
|
||||
selected: selected // determine if we should be selected
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ describe('Binder', function() {
|
|||
expect(html.indexOf('action="foo();"')).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('RepeaterAdd', inject(function($rootScope, $compile, $browser) {
|
||||
it('RepeaterAdd', inject(function($rootScope, $compile) {
|
||||
element = $compile('<div><input type="text" ng:model="item.x" ng:repeat="item in items"></div>')($rootScope);
|
||||
$rootScope.items = [{x:'a'}, {x:'b'}];
|
||||
$rootScope.$apply();
|
||||
|
|
@ -166,8 +166,7 @@ describe('Binder', function() {
|
|||
expect(second.val()).toEqual('b');
|
||||
|
||||
first.val('ABC');
|
||||
browserTrigger(first, 'keydown');
|
||||
$browser.defer.flush();
|
||||
browserTrigger(first, 'blur');
|
||||
expect($rootScope.items[0].x).toEqual('ABC');
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -1,206 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
describe('$formFactory', function() {
|
||||
|
||||
it('should have global form', inject(function($rootScope, $formFactory) {
|
||||
expect($formFactory.rootForm).toBeTruthy();
|
||||
expect($formFactory.rootForm.$createWidget).toBeTruthy();
|
||||
}));
|
||||
|
||||
|
||||
describe('new form', function() {
|
||||
var form;
|
||||
var scope;
|
||||
var log;
|
||||
|
||||
function WidgetCtrl($formFactory, $scope) {
|
||||
log += '<init>';
|
||||
$scope.$render = function() {
|
||||
log += '$render();';
|
||||
};
|
||||
$scope.$on('$validate', function(e){
|
||||
log += '$validate();';
|
||||
});
|
||||
|
||||
this.$formFactory = $formFactory;
|
||||
}
|
||||
|
||||
WidgetCtrl.$inject = ['$formFactory', '$scope'];
|
||||
|
||||
WidgetCtrl.prototype = {
|
||||
getFormFactory: function() {
|
||||
return this.$formFactory;
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(inject(function($rootScope, $formFactory) {
|
||||
log = '';
|
||||
scope = $rootScope.$new();
|
||||
form = $formFactory(scope);
|
||||
}));
|
||||
|
||||
describe('$createWidget', function() {
|
||||
var widget;
|
||||
|
||||
beforeEach(function() {
|
||||
widget = form.$createWidget({
|
||||
scope:scope,
|
||||
model:'text',
|
||||
alias:'text',
|
||||
controller:WidgetCtrl
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('data flow', function() {
|
||||
it('should have status properties', inject(function($rootScope, $formFactory) {
|
||||
expect(widget.$error).toEqual({});
|
||||
expect(widget.$valid).toBe(true);
|
||||
expect(widget.$invalid).toBe(false);
|
||||
}));
|
||||
|
||||
|
||||
it('should update view when model changes', inject(function($rootScope, $formFactory) {
|
||||
scope.text = 'abc';
|
||||
scope.$digest();
|
||||
expect(log).toEqual('<init>$validate();$render();');
|
||||
expect(widget.$modelValue).toEqual('abc');
|
||||
|
||||
scope.text = 'xyz';
|
||||
scope.$digest();
|
||||
expect(widget.$modelValue).toEqual('xyz');
|
||||
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
describe('validation', function() {
|
||||
it('should update state on error', inject(function($rootScope, $formFactory) {
|
||||
widget.$emit('$invalid', 'E');
|
||||
expect(widget.$valid).toEqual(false);
|
||||
expect(widget.$invalid).toEqual(true);
|
||||
|
||||
widget.$emit('$valid', 'E');
|
||||
expect(widget.$valid).toEqual(true);
|
||||
expect(widget.$invalid).toEqual(false);
|
||||
}));
|
||||
|
||||
|
||||
it('should have called the model setter before the validation', inject(function($rootScope, $formFactory) {
|
||||
var modelValue;
|
||||
widget.$on('$validate', function() {
|
||||
modelValue = scope.text;
|
||||
});
|
||||
widget.$emit('$viewChange', 'abc');
|
||||
expect(modelValue).toEqual('abc');
|
||||
}));
|
||||
|
||||
|
||||
describe('form', function() {
|
||||
it('should invalidate form when widget is invalid', inject(function($rootScope, $formFactory) {
|
||||
expect(form.$error).toEqual({});
|
||||
expect(form.$valid).toEqual(true);
|
||||
expect(form.$invalid).toEqual(false);
|
||||
|
||||
widget.$emit('$invalid', 'REASON');
|
||||
|
||||
expect(form.$error.REASON).toEqual([widget]);
|
||||
expect(form.$valid).toEqual(false);
|
||||
expect(form.$invalid).toEqual(true);
|
||||
|
||||
var widget2 = form.$createWidget({
|
||||
scope:scope, model:'text',
|
||||
alias:'text',
|
||||
controller:WidgetCtrl
|
||||
});
|
||||
widget2.$emit('$invalid', 'REASON');
|
||||
|
||||
expect(form.$error.REASON).toEqual([widget, widget2]);
|
||||
expect(form.$valid).toEqual(false);
|
||||
expect(form.$invalid).toEqual(true);
|
||||
|
||||
widget.$emit('$valid', 'REASON');
|
||||
|
||||
expect(form.$error.REASON).toEqual([widget2]);
|
||||
expect(form.$valid).toEqual(false);
|
||||
expect(form.$invalid).toEqual(true);
|
||||
|
||||
widget2.$emit('$valid', 'REASON');
|
||||
|
||||
expect(form.$error).toEqual({});
|
||||
expect(form.$valid).toEqual(true);
|
||||
expect(form.$invalid).toEqual(false);
|
||||
}));
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('id assignment', function() {
|
||||
it('should default to name expression', inject(function($rootScope, $formFactory) {
|
||||
expect(form.text).toEqual(widget);
|
||||
}));
|
||||
|
||||
|
||||
it('should use ng:id', inject(function($rootScope, $formFactory) {
|
||||
widget = form.$createWidget({
|
||||
scope:scope,
|
||||
model:'text',
|
||||
alias:'my.id',
|
||||
controller:WidgetCtrl
|
||||
});
|
||||
expect(form['my.id']).toEqual(widget);
|
||||
}));
|
||||
|
||||
|
||||
it('should not override existing names', inject(function($rootScope, $formFactory) {
|
||||
var widget2 = form.$createWidget({
|
||||
scope:scope,
|
||||
model:'text',
|
||||
alias:'text',
|
||||
controller:WidgetCtrl
|
||||
});
|
||||
expect(form.text).toEqual(widget);
|
||||
expect(widget2).not.toEqual(widget);
|
||||
}));
|
||||
});
|
||||
|
||||
describe('dealocation', function() {
|
||||
it('should dealocate', inject(function($rootScope, $formFactory) {
|
||||
var widget2 = form.$createWidget({
|
||||
scope:scope,
|
||||
model:'text',
|
||||
alias:'myId',
|
||||
controller:WidgetCtrl
|
||||
});
|
||||
expect(form.myId).toEqual(widget2);
|
||||
var widget3 = form.$createWidget({
|
||||
scope:scope,
|
||||
model:'text',
|
||||
alias:'myId',
|
||||
controller:WidgetCtrl
|
||||
});
|
||||
expect(form.myId).toEqual(widget2);
|
||||
|
||||
widget3.$destroy();
|
||||
expect(form.myId).toEqual(widget2);
|
||||
|
||||
widget2.$destroy();
|
||||
expect(form.myId).toBeUndefined();
|
||||
}));
|
||||
|
||||
|
||||
it('should remove invalid fields from errors, when child widget removed', inject(function($rootScope, $formFactory) {
|
||||
widget.$emit('$invalid', 'MyError');
|
||||
|
||||
expect(form.$error.MyError).toEqual([widget]);
|
||||
expect(form.$invalid).toEqual(true);
|
||||
|
||||
widget.$destroy();
|
||||
|
||||
expect(form.$error.MyError).toBeUndefined();
|
||||
expect(form.$invalid).toEqual(false);
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,122 +1,205 @@
|
|||
'use strict';
|
||||
|
||||
describe('form', function() {
|
||||
var doc;
|
||||
var doc, widget, scope, $compile;
|
||||
|
||||
beforeEach(module(function($compileProvider) {
|
||||
$compileProvider.directive('storeModelCtrl', function() {
|
||||
return {
|
||||
require: 'ngModel',
|
||||
link: function(scope, elm, attr, ctrl) {
|
||||
widget = ctrl;
|
||||
}
|
||||
};
|
||||
});
|
||||
}));
|
||||
|
||||
beforeEach(inject(function($injector) {
|
||||
$compile = $injector.get('$compile');
|
||||
scope = $injector.get('$rootScope');
|
||||
}));
|
||||
|
||||
afterEach(function() {
|
||||
dealoc(doc);
|
||||
});
|
||||
|
||||
|
||||
it('should attach form to DOM', inject(function($rootScope, $compile) {
|
||||
doc = angular.element('<form>');
|
||||
$compile(doc)($rootScope);
|
||||
it('should instantiate form and attach it to DOM', function() {
|
||||
doc = $compile('<form>')(scope);
|
||||
expect(doc.data('$form')).toBeTruthy();
|
||||
}));
|
||||
expect(doc.data('$form') instanceof FormController).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
it('should prevent form submission', inject(function($rootScope, $compile) {
|
||||
it('should remove the widget when element removed', function() {
|
||||
doc = $compile(
|
||||
'<form name="form">' +
|
||||
'<input type="text" name="alias" ng:model="value" store-model-ctrl/>' +
|
||||
'</form>')(scope);
|
||||
|
||||
var form = scope.form;
|
||||
widget.emitValidity('REQUIRED', false);
|
||||
expect(form.alias).toBe(widget);
|
||||
expect(form.error.REQUIRED).toEqual([widget]);
|
||||
|
||||
doc.find('input').remove();
|
||||
expect(form.error.REQUIRED).toBeUndefined();
|
||||
expect(form.alias).toBeUndefined();
|
||||
});
|
||||
|
||||
|
||||
it('should prevent form submission', function() {
|
||||
var startingUrl = '' + window.location;
|
||||
doc = angular.element('<form name="myForm"><input type=submit val=submit>');
|
||||
$compile(doc)($rootScope);
|
||||
doc = jqLite('<form name="myForm"><input type="submit" value="submit" />');
|
||||
$compile(doc)(scope);
|
||||
|
||||
browserTrigger(doc.find('input'));
|
||||
waitsFor(
|
||||
function() { return true; },
|
||||
'let browser breath, so that the form submision can manifest itself', 10);
|
||||
|
||||
runs(function() {
|
||||
expect('' + window.location).toEqual(startingUrl);
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
it('should not prevent form submission if action attribute present',
|
||||
inject(function($compile, $rootScope) {
|
||||
it('should not prevent form submission if action attribute present', function() {
|
||||
var callback = jasmine.createSpy('submit').andCallFake(function(event) {
|
||||
expect(event.isDefaultPrevented()).toBe(false);
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
doc = angular.element('<form name="x" action="some.py" />');
|
||||
$compile(doc)($rootScope);
|
||||
doc = $compile('<form name="x" action="some.py" />')(scope);
|
||||
doc.bind('submit', callback);
|
||||
|
||||
browserTrigger(doc, 'submit');
|
||||
expect(callback).toHaveBeenCalledOnce();
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
it('should publish form to scope', inject(function($rootScope, $compile) {
|
||||
doc = angular.element('<form name="myForm"></form>');
|
||||
$compile(doc)($rootScope);
|
||||
expect($rootScope.myForm).toBeTruthy();
|
||||
it('should publish form to scope', function() {
|
||||
doc = $compile('<form name="myForm"></form>')(scope);
|
||||
expect(scope.myForm).toBeTruthy();
|
||||
expect(doc.data('$form')).toBeTruthy();
|
||||
expect(doc.data('$form')).toEqual($rootScope.myForm);
|
||||
}));
|
||||
expect(doc.data('$form')).toEqual(scope.myForm);
|
||||
});
|
||||
|
||||
|
||||
it('should have ng-valide/ng-invalid style', inject(function($rootScope, $compile) {
|
||||
doc = angular.element('<form name="myForm"><input type=text ng:model=text required>');
|
||||
$compile(doc)($rootScope);
|
||||
$rootScope.text = 'misko';
|
||||
$rootScope.$digest();
|
||||
it('should allow name to be an expression', function() {
|
||||
doc = $compile('<form name="obj.myForm"></form>')(scope);
|
||||
|
||||
expect(doc.hasClass('ng-valid')).toBe(true);
|
||||
expect(doc.hasClass('ng-invalid')).toBe(false);
|
||||
|
||||
$rootScope.text = '';
|
||||
$rootScope.$digest();
|
||||
expect(doc.hasClass('ng-valid')).toBe(false);
|
||||
expect(doc.hasClass('ng-invalid')).toBe(true);
|
||||
}));
|
||||
expect(scope.obj).toBeDefined();
|
||||
expect(scope.obj.myForm).toBeTruthy();
|
||||
});
|
||||
|
||||
|
||||
it('should chain nested forms', inject(function($rootScope, $compile) {
|
||||
doc = angular.element(
|
||||
'<ng:form name=parent>' +
|
||||
'<ng:form name=child>' +
|
||||
'<input type=text ng:model=text name=text>' +
|
||||
it('should chain nested forms', function() {
|
||||
doc = jqLite(
|
||||
'<ng:form name="parent">' +
|
||||
'<ng:form name="child">' +
|
||||
'<input type="text" ng:model="text" name="text">' +
|
||||
'</ng:form>' +
|
||||
'</ng:form>');
|
||||
$compile(doc)($rootScope);
|
||||
var parent = $rootScope.parent;
|
||||
var child = $rootScope.child;
|
||||
$compile(doc)(scope);
|
||||
|
||||
var parent = scope.parent;
|
||||
var child = scope.child;
|
||||
var input = child.text;
|
||||
|
||||
input.$emit('$invalid', 'MyError');
|
||||
expect(parent.$error.MyError).toEqual([input]);
|
||||
expect(child.$error.MyError).toEqual([input]);
|
||||
input.emitValidity('MyError', false);
|
||||
expect(parent.error.MyError).toEqual([input]);
|
||||
expect(child.error.MyError).toEqual([input]);
|
||||
|
||||
input.$emit('$valid', 'MyError');
|
||||
expect(parent.$error.MyError).toBeUndefined();
|
||||
expect(child.$error.MyError).toBeUndefined();
|
||||
}));
|
||||
input.emitValidity('MyError', true);
|
||||
expect(parent.error.MyError).toBeUndefined();
|
||||
expect(child.error.MyError).toBeUndefined();
|
||||
});
|
||||
|
||||
|
||||
it('should chain nested forms in repeater', inject(function($rootScope, $compile) {
|
||||
doc = angular.element(
|
||||
it('should chain nested forms in repeater', function() {
|
||||
doc = jqLite(
|
||||
'<ng:form name=parent>' +
|
||||
'<ng:form ng:repeat="f in forms" name=child>' +
|
||||
'<input type=text ng:model=text name=text>' +
|
||||
'</ng:form>' +
|
||||
'</ng:form>');
|
||||
$compile(doc)($rootScope);
|
||||
$rootScope.forms = [1];
|
||||
$rootScope.$digest();
|
||||
$compile(doc)(scope);
|
||||
|
||||
var parent = $rootScope.parent;
|
||||
scope.$apply(function() {
|
||||
scope.forms = [1];
|
||||
});
|
||||
|
||||
var parent = scope.parent;
|
||||
var child = doc.find('input').scope().child;
|
||||
var input = child.text;
|
||||
|
||||
expect(parent).toBeDefined();
|
||||
expect(child).toBeDefined();
|
||||
expect(input).toBeDefined();
|
||||
|
||||
input.$emit('$invalid', 'myRule');
|
||||
expect(input.$error.myRule).toEqual(true);
|
||||
expect(child.$error.myRule).toEqual([input]);
|
||||
expect(parent.$error.myRule).toEqual([input]);
|
||||
input.emitValidity('myRule', false);
|
||||
expect(input.error.myRule).toEqual(true);
|
||||
expect(child.error.myRule).toEqual([input]);
|
||||
expect(parent.error.myRule).toEqual([input]);
|
||||
|
||||
input.$emit('$valid', 'myRule');
|
||||
expect(parent.$error.myRule).toBeUndefined();
|
||||
expect(child.$error.myRule).toBeUndefined();
|
||||
}));
|
||||
input.emitValidity('myRule', true);
|
||||
expect(parent.error.myRule).toBeUndefined();
|
||||
expect(child.error.myRule).toBeUndefined();
|
||||
});
|
||||
|
||||
|
||||
it('should publish widgets', function() {
|
||||
doc = jqLite('<form name="form"><input type="text" name="w1" ng:model="some" /></form>');
|
||||
$compile(doc)(scope);
|
||||
|
||||
var widget = scope.form.w1;
|
||||
expect(widget).toBeDefined();
|
||||
expect(widget.pristine).toBe(true);
|
||||
expect(widget.dirty).toBe(false);
|
||||
expect(widget.valid).toBe(true);
|
||||
expect(widget.invalid).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
describe('validation', function() {
|
||||
|
||||
beforeEach(function() {
|
||||
doc = $compile(
|
||||
'<form name="form">' +
|
||||
'<input type="text" ng:model="name" name="name" store-model-ctrl/>' +
|
||||
'</form>')(scope);
|
||||
|
||||
scope.$digest();
|
||||
});
|
||||
|
||||
|
||||
it('should have ng-valid/ng-invalid css class', function() {
|
||||
expect(doc).toBeValid();
|
||||
|
||||
widget.emitValidity('ERROR', false);
|
||||
scope.$apply();
|
||||
expect(doc).toBeInvalid();
|
||||
|
||||
widget.emitValidity('ANOTHER', false);
|
||||
scope.$apply();
|
||||
|
||||
widget.emitValidity('ERROR', true);
|
||||
scope.$apply();
|
||||
expect(doc).toBeInvalid();
|
||||
|
||||
widget.emitValidity('ANOTHER', true);
|
||||
scope.$apply();
|
||||
expect(doc).toBeValid();
|
||||
});
|
||||
|
||||
|
||||
it('should have ng-pristine/ng-dirty css class', function() {
|
||||
expect(doc).toBePristine();
|
||||
|
||||
widget.touch();
|
||||
scope.$apply();
|
||||
expect(doc).toBeDirty();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue