aboutsummaryrefslogtreecommitdiffstats
path: root/src/service
diff options
context:
space:
mode:
authorMisko Hevery2011-09-08 13:56:29 -0700
committerIgor Minar2011-10-11 11:01:45 -0700
commit4f78fd692c0ec51241476e6be9a4df06cd62fdd6 (patch)
tree91f70bb89b9c095126fbc093f51cedbac5cb0c78 /src/service
parentdf6d2ba3266de405ad6c2f270f24569355706e76 (diff)
downloadangular.js-4f78fd692c0ec51241476e6be9a4df06cd62fdd6.tar.bz2
feat(forms): new and improved forms
Diffstat (limited to 'src/service')
-rw-r--r--src/service/formFactory.js394
-rw-r--r--src/service/invalidWidgets.js69
-rw-r--r--src/service/log.js3
-rw-r--r--src/service/resource.js3
-rw-r--r--src/service/route.js6
-rw-r--r--src/service/window.js2
-rw-r--r--src/service/xhr.js5
7 files changed, 406 insertions, 76 deletions
diff --git a/src/service/formFactory.js b/src/service/formFactory.js
new file mode 100644
index 00000000..4fc53935
--- /dev/null
+++ b/src/service/formFactory.js
@@ -0,0 +1,394 @@
+'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 <a href="#form">form</a> 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(){
+ this.html = '<b>Hello</b> <i>World</i>!';
+ }
+
+ function HTMLEditorWidget(element) {
+ var self = this;
+ var htmlFilter = angular.filter('html');
+
+ this.$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');
+ }
+ }
+
+ this.$render = function(){
+ element.html(this.$viewValue);
+ }
+
+ element.bind('keyup', function(){
+ self.$apply(function(){
+ self.$emit('$viewChange', element.html());
+ });
+ });
+ }
+
+ angular.directive('ng:contenteditable', function(){
+ function linkFn($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]});
+ // if the element is destroyed, then we need to notify the form.
+ element.bind('$destroy', function(){
+ widget.$destroy();
+ });
+ }
+ linkFn.$inject = ['$formFactory'];
+ return linkFn;
+ });
+ </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>
+ */
+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 `<div ng:show="form.$error.KEY">error for key</div>`.
+ */
+
+/**
+ * @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 `<div ng:show="form.$error.KEY">error for key</div>`.
+ */
+
+/**
+ * @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;
+};
diff --git a/src/service/invalidWidgets.js b/src/service/invalidWidgets.js
deleted file mode 100644
index 7c1b2a9f..00000000
--- a/src/service/invalidWidgets.js
+++ /dev/null
@@ -1,69 +0,0 @@
-'use strict';
-
-/**
- * @workInProgress
- * @ngdoc service
- * @name angular.service.$invalidWidgets
- *
- * @description
- * Keeps references to all invalid widgets found during validation.
- * Can be queried to find whether there are any invalid widgets currently displayed.
- *
- * @example
- */
-angularServiceInject("$invalidWidgets", function(){
- var invalidWidgets = [];
-
-
- /** Remove an element from the array of invalid widgets */
- invalidWidgets.markValid = function(element){
- var index = indexOf(invalidWidgets, element);
- if (index != -1)
- invalidWidgets.splice(index, 1);
- };
-
-
- /** Add an element to the array of invalid widgets */
- invalidWidgets.markInvalid = function(element){
- var index = indexOf(invalidWidgets, element);
- if (index === -1)
- invalidWidgets.push(element);
- };
-
-
- /** Return count of all invalid widgets that are currently visible */
- invalidWidgets.visible = function() {
- var count = 0;
- forEach(invalidWidgets, function(widget){
- count = count + (isVisible(widget) ? 1 : 0);
- });
- return count;
- };
-
-
- /* At the end of each eval removes all invalid widgets that are not part of the current DOM. */
- this.$watch(function() {
- for(var i = 0; i < invalidWidgets.length;) {
- var widget = invalidWidgets[i];
- if (isOrphan(widget[0])) {
- invalidWidgets.splice(i, 1);
- if (widget.dealoc) widget.dealoc();
- } else {
- i++;
- }
- }
- });
-
-
- /**
- * Traverses DOM element's (widget's) parents and considers the element to be an orphan if one of
- * it's parents isn't the current window.document.
- */
- function isOrphan(widget) {
- if (widget == window.document) return false;
- var parent = widget.parentNode;
- return !parent || isOrphan(parent);
- }
-
- return invalidWidgets;
-});
diff --git a/src/service/log.js b/src/service/log.js
index 09945732..3dacd117 100644
--- a/src/service/log.js
+++ b/src/service/log.js
@@ -18,12 +18,13 @@
<script>
function LogCtrl($log) {
this.$log = $log;
+ this.message = 'Hello World!';
}
</script>
<div ng:controller="LogCtrl">
<p>Reload this page with open console, enter text and hit the log button...</p>
Message:
- <input type="text" name="message" value="Hello World!"/>
+ <input type="text" ng:model="message"/>
<button ng:click="$log.log(message)">log</button>
<button ng:click="$log.warn(message)">warn</button>
<button ng:click="$log.info(message)">info</button>
diff --git a/src/service/resource.js b/src/service/resource.js
index f6e0be18..915f2d92 100644
--- a/src/service/resource.js
+++ b/src/service/resource.js
@@ -160,6 +160,7 @@
<doc:source jsfiddle="false">
<script>
function BuzzController($resource) {
+ this.userId = 'googlebuzz';
this.Activity = $resource(
'https://www.googleapis.com/buzz/v1/activities/:userId/:visibility/:activityId/:comments',
{alt:'json', callback:'JSON_CALLBACK'},
@@ -179,7 +180,7 @@
</script>
<div ng:controller="BuzzController">
- <input name="userId" value="googlebuzz"/>
+ <input ng:model="userId"/>
<button ng:click="fetch()">fetch</button>
<hr/>
<div ng:repeat="item in activities.data.items">
diff --git a/src/service/route.js b/src/service/route.js
index 73c73b04..b78cca91 100644
--- a/src/service/route.js
+++ b/src/service/route.js
@@ -260,7 +260,8 @@ angularServiceInject('$route', function($location, $routeParams) {
function updateRoute() {
var next = parseRoute(),
- last = $route.current;
+ last = $route.current,
+ Controller;
if (next && last && next.$route === last.$route
&& equals(next.pathParams, last.pathParams) && !next.reloadOnSearch && !forceReload) {
@@ -283,7 +284,8 @@ angularServiceInject('$route', function($location, $routeParams) {
}
} else {
copy(next.params, $routeParams);
- next.scope = parentScope.$new(next.controller);
+ (Controller = next.controller) && inferInjectionArgs(Controller);
+ next.scope = parentScope.$new(Controller);
}
}
rootScope.$broadcast('$afterRouteChange', next, last);
diff --git a/src/service/window.js b/src/service/window.js
index 2f3f677a..9795e4fc 100644
--- a/src/service/window.js
+++ b/src/service/window.js
@@ -17,7 +17,7 @@
* @example
<doc:example>
<doc:source>
- <input ng:init="$window = $service('$window'); greeting='Hello World!'" type="text" name="greeting" />
+ <input ng:init="$window = $service('$window'); greeting='Hello World!'" type="text" ng:model="greeting" />
<button ng:click="$window.alert(greeting)">ALERT</button>
</doc:source>
<doc:scenario>
diff --git a/src/service/xhr.js b/src/service/xhr.js
index 09e7d070..4981c078 100644
--- a/src/service/xhr.js
+++ b/src/service/xhr.js
@@ -111,6 +111,7 @@
<script>
function FetchCntl($xhr) {
var self = this;
+ this.url = 'index.html';
this.fetch = function() {
self.code = null;
@@ -133,11 +134,11 @@
FetchCntl.$inject = ['$xhr'];
</script>
<div ng:controller="FetchCntl">
- <select name="method">
+ <select ng:model="method">
<option>GET</option>
<option>JSON</option>
</select>
- <input type="text" name="url" value="index.html" size="80"/>
+ <input type="text" ng:model="url" size="80"/>
<button ng:click="fetch()">fetch</button><br>
<button ng:click="updateModel('GET', 'index.html')">Sample GET</button>
<button ng:click="updateModel('JSON', 'http://angularjs.org/greet.php?callback=JSON_CALLBACK&name=Super%20Hero')">Sample JSONP</button>