diff options
| author | Misko Hevery | 2011-09-08 13:56:29 -0700 | 
|---|---|---|
| committer | Igor Minar | 2011-10-11 11:01:45 -0700 | 
| commit | 4f78fd692c0ec51241476e6be9a4df06cd62fdd6 (patch) | |
| tree | 91f70bb89b9c095126fbc093f51cedbac5cb0c78 /src/service | |
| parent | df6d2ba3266de405ad6c2f270f24569355706e76 (diff) | |
| download | angular.js-4f78fd692c0ec51241476e6be9a4df06cd62fdd6.tar.bz2 | |
feat(forms): new and improved forms
Diffstat (limited to 'src/service')
| -rw-r--r-- | src/service/formFactory.js | 394 | ||||
| -rw-r--r-- | src/service/invalidWidgets.js | 69 | ||||
| -rw-r--r-- | src/service/log.js | 3 | ||||
| -rw-r--r-- | src/service/resource.js | 3 | ||||
| -rw-r--r-- | src/service/route.js | 6 | ||||
| -rw-r--r-- | src/service/window.js | 2 | ||||
| -rw-r--r-- | src/service/xhr.js | 5 | 
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> | 
