10 Jul

Sticky Validation – Angular 1.5.7

Why was this created

Sticky validation is meant to fill the gap in Angular when you only want to show errors on submit,

and leave those errors displayed even after the user has started changing the input that had the errors.

Typically in Angular they operate on the concept of dynamic validation.

This validation evaluates the model in real time and adjusts the error messages accordingly based on user input.

Once the user has typed something else the original error that caused the problem has been removed automatically by Angular (provided they corrected that original error)

and the model value change has either became valid or possibly still invalid with another validation error occurring.

With the current implementation of Angular 1.5.7 there was no way to keep the original error and invalid state set once the user fixed

the problem. From a UX perspective consistency is vital and since we want to operate in the context of only show errors on submit,

the existing errors should be visible until a submit was again performed.

What is sticky validation

Sticky validation allows you to work with the pre-existing ng-messages/ng-message directives provided by Angular.

This injects new properties on the form and $error objects to allow for a constant state to be available when displaying error messages

to the user. Sticky validation also operates on the premise that you want to implement "submit" only validation (although nothing is prohibiting sticky state properties from being used with regular validation).

Sticky validation will allow you to preserve the original error message displayed and have a consistent property available indicating

that "submits" are happening (currently in Angular if you try and use $submitted and $invalid together to show the messages, once $invalid becomes false the messages will hide).

How to implement

Implementing can be done in 3 easy steps, once the directive has been added to your project and registered with your Angular app.

  1. Replace any existing ng-submit with se-submit

    <form name="cntrl.form" se-submit="cntrl.submit()" novalidate>
    

  2. To ensure ng-messages only display when the form is submitted and invalid. Add the following in the ng-if.

    $fieldInvalid is the custom property added to each element that is kept in sync with Angulars’ $invalid property

    <div ng-messages="cntrl.form.badgenumber.$error" ng-if="cntrl.form.$submitted && cntrl.form.badgenumber.$fieldInvalid">
    
        all the ng-message directives
    
    </div>        
    

  3. In your ng-message elements switch the current validation name “required” (Angular implementation) with “stickyrequired”

    <div ng-message="stickyrequired" class="has-error">
    
        Badge Number must be provided
    
    </div>        
    

Pitfalls

Currently this directive only works for a single level form. If your form has child forms nested within, this will NOT inject

the custom properties. I have not come across many cases where usage of sub forms is needed, but there are always outliers.

If child form support is needed please feel free to submit a pull request for review.

Example Walkthrough

  1. User comes to page and sees the form to fill out

    alt text

  2. User types invalid characters into the form field and clicks “Continue”

    alt text

    The error is displayed in standard Angular fashion

  3. With use of sticky validation, once the field is cleared (which sets its state back to valid). the error message and error highlighting remain intact

    alt text

  4. User corrects the issue and clicks “Continue” again. That validation issue is then re-evaluated and cleared accordingly.

    alt text

Final Words

I hope this helps in overcoming the pitfalls and gaps with Angular validation when it comes to not wanting dynamic validation on all the time.
One of the great things about this is it still allows you to use Angulars’ built in validation for dynamic error messaging. Want the source code? Visit my GitHub

For more information view

ng-messages

angular forms

9 Jul

Scroll watcher directive for Angular 1.5.7

Here is a directive I came up with to help with keeping track of page scroll position and when scrolling has started and stopped. I had a need for this in trying to hide page content while the user was scrolling up/down a page, and then re-showing the content once the scrolling had stopped. Currently this is only setup up to work at the document level, but and easy modification could be made to allow a new property to drive what scroll area is being monitored. I hope this helps others in case they need a way to tell if page scrolling has started or stopped.

How to Implement

HTML
Simply add the directive to the page you want to monitor scrolling on. Next add the scroll-callback function you want to be called from directive when scrolling starts and stops

<div page-scroll-watcher scroll-callback="cntl.scrollStop($event, isEndEvent, isScrollingEvent)">

Callback Function
Note: sample code is in ES6 format. This is an excerpt from a angular controller

 //$event is the standard scroll event from the browser. This contains the X,Y information
 //isEndEvent signals when scrolling has stopped
 //isScrollingEvent signals when scrolling has started
 scrollStop($event, isEndEvent, isScrollingEvent) {
    if (isEndEvent) {
      this.showBottomBar = true;
      return;
    }
    if(isScrollingEvent)
    {
      this.showBottomBar = false;
      return;
    }
  }

Now that we have the how to implement lets get to the good stuff. The code that makes this all work
Page Scroll Directive
Note: code is in ES6 format


//this would just need to be registered with your Angular app
import angular from "angular";
import * as _ from "lodash";

const directivesModule = angular.module("MyDirectives", [])
  .directive("pageScrollWatcher", ["$window", "$document", pageScrollWatcher]);

function pageScrollWatcher($document) {
  return {
    restrict: "A",
    scope: {
      scrollCallback: "&"
    },
    link: function (scope) {
      //here could be updated to use the element this directive is attached to if needed to watch a scrollable div container
      const el = angular.element($document); 

      //here we delay evaluating the scrolling events until they have stopped
      const dbnce = _.debounce(function (e) {
        //send event that scrolling stopped
        scope.$apply(function () {
          //execute the provided callback
          scope.scrollCallback({ $event: e, isEndEvent: true, isScrollingEvent: false });
        });

        //register first scroll interceptor. Since scrolling has stopped we now need to register a start scrolling event binding
        el.bind("scroll", firstScrollFunc);

      }, 200);

      const firstScrollFunc = function (e) {
        //so we have detected the scrolling needs to start. Since this is a one time event between starts/stops we need to
        //unregister the start scrolling event
        el.unbind("scroll", firstScrollFunc);
        scope.$apply(function () {
          //execute the provided callback
          scope.scrollCallback({ $event: e, isEndEvent: false, isScrollingEvent: true });
          //We do this incase angular removes dom parts causing the scroll bar to disappear or change.
          //we need to trigger the end event again 
          dbnce(e);
        });
      };

      //on first load of directive register the start and stop events
      el.bind("scroll", firstScrollFunc);
      el.bind("scroll", dbnce);

      scope.$on("$destroy", function handleDestroyEvent() {
        //when switching pages remove event
        el.unbind("scroll", dbnce);
        el.unbind("scroll", firstScrollFunc);
      });

    }
  };
}

Want the source? Visit my GitHub