exhtml
1/10/2018 - 6:58 AM

Angular Forms and Validation

ng-submit, ng-model, ng-options, ng-disabled... *ng-options sintaxis compleja, ampliar: https://www.undefinednull.com/2014/08/11/a-brief-walk-through-of-the-ng-options-in-angularjs/


//
// Two way data binding in directives: ngModel require, $setViewValue, $render
//
// Example: we'll set two way data binding throught $setViewValue / $render. 
// (Use case: when working with jquery plugins and want to bind the results/manipulations to angular)
<div ng-controller="HTMLController as html">
  <div html-editor contenteditable ng-model="html.content"></div> <!-- Bind an angular model to a 'contenteditable' html element -->
  <p>{{ html.content }}</p> 
  <div>
    <a href="" ng-click="html.reset()">Reset</a>
  </div>
</div>
//
// directive htmlEditor
HTMLEditor.js
function HTMLEditor() {
  return {
    require: 'ngModel', //we've bound an ng-model to the element. 'require' ngModel gives us the controller that powers ngModel
      link: function ($scope, $element, $attrs, $ctrl) { //we pass the $ctrl as the fourth parameter
		    console.log($ctrl); //inspect properties and methods bound to the ngModel controller. 
        //The ones we're going to use are $render and $setViewValue
        var ngModelCtrl = $ctrl; //$ctrl is the 'ngModel' controller. It does two-way databinding:
        // View -> Model
        $element.on('input', function (event) { //the contenteditable element fires an 'input' event, and gives us the event object back
          ngModelCtrl.$setViewValue(event.target.innerHTML); //we obtain the innerHTML of the target element ('view value') and pass it to the Model
        });
        // Model -> View
        // when the model change, pass it to the view
        ngModelCtrl.$render = function () {
          $element.html(ngModelCtrl.$modelValue); //take the 'model value' associated with the controller and pass it to the html
        };
        ngModelCtrl.$setViewValue($element[0].innerHTML); //to do this at runtime (as we don't have 'event' here
      }
    };
}


//
//$parsers and $formatters: control the Model > View and View > Model values
//

// Works similar way to $setViewValue / $render.
<input type="text"
  coupon-format> //directive that controls the 'view display' and the 'model display' of the ng-model

//couponFormat.js
function couponFormat() {
  return {
    require: 'ngModel',
    link: function ($scope, $elem, $attrs, $ctrl) {
      var ngModelCtrl = $ctrl;
      ngModelCtrl.$formatters.unshift(function (value) { //$formatters are an array/collection of functions that get run against our model values. 'unshift' function will run as our model gets updated, so we can manipulate the value before returning it to the view

        // Model to View
        // transform coupon: 'summer-50' -> 'SUMMER_50'
        return value.replace(/-/g, '_').toUpperCase();
      });

      ngModelCtrl.$parsers.unshift(function (value) {
        // View to Model
        // transform coupon: 'SUMMER_50' -> 'summer-50'
        return value.replace(/_/g, '-').toLowerCase();
      });
    }
  }
}

angular
    .module('app')
    .directive('couponFormat', couponFormat);

// *In these ways we can create reusable directives to use in any input in our forms.


//
// $validators pipeline: create custom error properties: example - validate field against regular expression
//

<form novalidate name="register"
  ng-controller="CheckoutController as checkout"
  ng-submit="checkout.onSubmit();">

<input type="text" required="required" name="coupon">

//We have the $error object for form validation
<pre>{{ register.coupon.$error | json }}</pre>
//
//use $validators to create a custom rule to validate this 'coupon' field: 
//the field would be invalid if the value doesn't match the regexp

//couponFormat.js
function couponFormat() {

  ngModelCtrl.$validators.coupon = function (modelValue, viewValue) { //unlike $parsers and $formatters, $validators is not an array, so we can create a custom property called 'coupon'. This allows us to create custom properties to show custom error messages to the user
    var COUPON_REGEX = /[A-Z]+\_\d{2}/;
    return COUPON_REGEX.test(viewValue); //we're testing 'SUMMER_50', not 'summer-50'
  };


//Now if we enter an invalid coupon int he field, the printed $error is:
{
  coupon: true; //coupon error is true
}
//So we can show an error message:
<div ng-if="register.coupon.$error.coupon"> //evaluating our 'coupon' error property!
 Invalid coupon! Must be characters, followed by an underscore and 2 digits.
</div>
//
//*So $validators allows us to create custom error properties based on an expression that resolves to a boolean.

//
// Configuring the form component
// (passing data and delegating submit function via bindings)
//

//parent controller:
this.submitOrder = function () {
    // communicate with API or backend
    console.log('Submitted!', this.customerOrder);
  };

//instantiating the conponent
<order-form data="order.customerOrder" submit="order.submitOrder();"></order-form>
//
//component controller
bindToController: {
      data: '=',
      submit: '&' //this '&' allows us to delegate functions into our directive under a specific alias, in this case, 'submit'
    },
...
this.onSubmit = function () {
    // pre-processing
    this.submit();
  };
//view
<form name="orderForm" novalidate ng-submit="form.onSubmit();">


//
// Binding form to controller data
// via ng-model and ng-options
//

// We bind all the fields in the form to the properties in the controller model via ng-model:
<input name="name" required type="text" ng-model="form.data.name" placeholder="Your name">

// Instead of creating an ng-repeat for the options in a select, use ng-options :
//controller
this.departments = [
  {
    "id": 1,
    "name": "Frontend Development"
  },
  {
    "id": 2,
    "name": "Backend Development"
  },
  {
    "id": 3,
    "name": "UX/UI Design"
  },
  {
    "id": 4,
    "name": "Product Management"
  }
]

//view
<select
  name="departmentId"
  required=""
  ng-model="$ctrl.contact.departmentId"
  ng-options="department.id as department.name for department in $ctrl.departments">
  <option value="">Select...</option>
</select>

//If we print
<pre>{{ orderForm.orderChoice | json }}</pre>
//
//we can see how angular updates the model as we change the values in the select.
{
	"$viewValue": {
		"label": "Product One",
		"id": 1
	},
	"$modelValue": {
		"label": "Product One",
		"id": 1
	},
	...
//and also if we inspect the form data:
<pre>{{ form.data | json }}</pre>
{
  "name": "",
  "email": "",
  "location": "",
  "product": {
    "label": "Product One",
    "id": 1
  },
  "comments": ""
}


//
// Internal validation states
//

// give the form a name, and add 'novalidate' to disables the browser validation, so we can manage it with Angular
<form name="orderForm" novalidate ng-submit="form.onSubmit();"> //
// give a name to each field in the form --*e get validation bound to native html attributes, such as type="email"
<input name="name" required="" type="text" ng-model="form.data.name" placeholder="Your name">
// * Check: Angular adds css classes for states of validation: ng-valid, ng-invalid etc
// inspect the form object:
<pre>{{ orderForm | json }}</pre>
// You can also inspect only the form fields as we fill them:
<pre>{{ form.data | json }}</pre>
//Or inspect individual fields:
<pre>{{ orderForm.name | json }}</pre>

//
//If we have 'required' fields we'll have content in the 'error' property:
{
  "$error": {
    "required": [
      {
        "$viewValue": "",
        "$modelValue": "",
       ...
  "$name": "orderForm",
  "$dirty": true,
  "$pristine": false,
  "$valid": true,
  "$invalid": false,
  "$submitted": false,

//For each input:
  "name": {
    "$viewValue": "sf",
    "$modelValue": "sf",
    "$validators": {},
    "$asyncValidators": {},
    "$parsers": [],
    "$formatters": [
      null
    ],
    "$viewChangeListeners": [],

//These change in the moment we 'touch' the input (we insert a value, and then we may delet it):
    "$untouched": false,
    "$touched": true,
    "$pristine": false, //true if we haven't entered any information
    "$dirty": true, //true if we have entered any information

    "$valid": true,
    "$invalid": false,
    "$error": {
		"required": true //if we have a 'required' attribute in our input
	},
    "$name": "name",
    "$options": null
  },
  "email": {
  ...
  },
  "orderChoice": {
    "$viewValue": {
      "label": "Product Two",
      "id": 2
    },
    "$modelValue": {
      "label": "Product Two",
      "id": 2
    },
    ...
  },

}


//
// Data-driven validation errors
//
<input name="name" required="" type="text" ng-model="form.data.name" placeholder="Your name">
<div
  ng-show="orderForm.name.$error.required && orderForm.name.$touched"> //para asegurarnos de que solo aparece cuando el input haya sido utilizado
  Name is required!
</div>
//
//for the email:
<input name="email" required="" type="email" ng-model="form.data.email" placeholder="Your email">
<div
  ng-show="orderForm.email.$error.required && orderForm.email.$touched"> //evaluamos si se ha completado el campo
  Email is required!
</div>
<div
  ng-show="orderForm.email.$error.email && orderForm.email.$touched"> //evaluamos si el email es válido
  Email is invalid!
</div>

//
// ng-disabled
//
// as we have properties for checking the validity of the whole form and each of the inputs:
"$valid": true,
"$invalid": false,
//we can disable the submit button until the form is valid:
<button type="submit" ng-disabled="orderForm.$invalid">

//
// Validation state-based CSS
//

//Angular applies automatically some classes to the input based on its state:
<input type="text" class="ng-pristine ng-empty ng-invalid ng-invalid-required ng-touched">
//So we can create custom css styles for that:
form.ng-dirty.ng-invalid { border: 1px solid red; }
form.ng-valid { border:1px solid green; }
// *pristine is that the user has interacted with the form –> becomes 'dirty'
// *touched is that user has make focus/flur