tl;dr: Live Demo | Source | API Docs
Whats a DSL?
No I’m not talking about that internet that made you put all those “line filters” on your land-line phones. The DSL I’m thinking of stands for Domain Specific Language. What does that mean?
Very simply it is a programming language that anyone can easily invent that is very specifically designed to efficient at describing and coding for your particular domain or problem you are trying to solve. This is in contrast different from a more general purpose language like HTML, JavaScript or C# which may not be as efficient at solving any problem.
The benefits of using a DSL can be quite simply way less code has to be written to solve a particular coding problem. The ancillary benefits of this can include better quality, productivity, reusability etc.
Angular directives let you DSL your HTML!
One of the coolest features about Angular is it allows you to specify custom HTML elements for you domain/problem set which can then interact with the DOM/JavaScript. Here is a great example of why you don’t google for slideshows on the internet (because of course you invariable get cats every time, oh internet…)
For this markup:
<slideshow title="Shocked Cats"> <slide src="cat1.jpg"></slide> <slide src="cat2.jpg"></slide> <!-- ... --> </slideshow>
You could generate something like this:
In this case the Angular directives <slideshow> and <slide> are used to generate a verbose amount of HTML required for display and wire it up to the JavaScript that makes it interactive.
Sweet! Here are some pretty awesome benefits I can derive from doing something like this:
- You can have designers/non developers with no programming experience build some pretty complex functionality and implement it frequently across an entire website.
- Isolation from change: if the business decides the cat slideshow style isn’t cutting it anymore everything about the underlying HTML markup can change with no impact on the DSL
Minification?
You know that thing that people do to JavaScript that makes it horribly ugly, impossible to debug and all errors occur on one line right? Here is a preview of good old Angular in minified form:
The benefits of minification of JavaScript (beyond the quite obvious obfuscation and removal of comments) is a slight reduction on file size after gzipping. In other words why not?
An Example Form
Now your probably wondering why the heck I would advocate for taking some perfectly tearse <slideshow> <slide> markup and turn it into <ss> <sl>. Don’t worry we aren’t going there. However…
Twitter Bootstrap has some verbose (yet flexible) markup
If you’ve ever tried making a form intensive application with Twitter Bootstrap you know what I’m talking about. Lets say you want something like this, a simple form with a few fields on it:
Looks easy enough right? Well take a look at the HTML required to pull it off:
<div class="form-horizontal"> <div class="form-group"> <label for="email" class="col-sm-2 control-label"> Email * </label> <div class="col-sm-3"> <input type="email" class="form-control" name="email" id="email" required> </div> </div> <div class="form-group"> <label for="firstName" class="col-sm-2 control-label"> First Name * </label> <div class="col-sm-3"> <input type="text" class="form-control" name="firstName" id="firstName" required> </div> </div> <div class="form-group"> <label for="lastName" class="col-sm-2 control-label"> Last Name Custom * </label> <div class="col-sm-3"> <input type="text" class="form-control" name="lastName" id="lastName" required> </div> </div> <div class="form-group"> <label for="favoriteNumber" class="col-sm-2 control-label"> Favorite Number </label> <div class="col-sm-3"> <input type="number" required class="form-control" name="favoriteNumber" id="favoriteNumber"> </div> </div> <div class="form-group"> <label for="age" class="col-sm-2 control-label"> Age * </label> <div class="col-sm-3"> <input type="number" class="form-control" name="age" id="age" min="0" max="140" required> </div> </div> <div class="form-group"> <label for="favoriteLetter" class="col-sm-2 control-label"> Favorite Letter * </label> <div class="col-sm-3"> <input type="text" class="form-control" name="favoriteLetter" id="favoriteLetter" maxlength="1" required> </div> </div> <div class="form-group"> <label for="favoriteWebsite" class="col-sm-2 control-label"> Favorite Website * </label> <div class="col-sm-3"> <input type="text" class="form-control" name="favoriteWebsite" id="favoriteWebsite" required> </div> </div> <div class="form-group"> <label for="favoriteColor" class="col-sm-2 control-label"> Favorite Color * </label> <div class="col-sm-3"> <select required> <option id="favoriteColor" required>Select...</option> <!-- some options here ---> </select> </div> </div> <div class="form-group"> <label class="col-sm-2 control-label">Gender *</label> <div class="col-sm-1"> <label class="radio-inline"> <input type="radio" name="gender" required aa-val-msg value="male"> Male </label> <label class="radio-inline"> <input type="radio" name="gender" required value="female"> Female </label> </div> </div> <div class="form-group"> <label class="col-sm-2 control-label">Gender *</label> <div class="col-sm-1"> <label class="radio-inline"> <input type="radio" name="gender" required value="male"> Male </label> <label class="radio-inline"> <input type="radio" name="gender" required value="female"> Female </label> </div> </div> </div>
Thats pretty verbose. This is just a static form with no degree of interactivity even and it’s that much HTML. Lets take it to the next level…
The same “verbose” form in Angular.js with validation
You thought the first one was long… Take a look at this. Now we have applied some sensical validation rules to the form, made most of the fields required and display a message if any of the rules are broken:
<div ng-form="exampleForm" class="form-horizontal"> <div class="form-group"> <label for="email" class="col-sm-2 control-label"> Email * </label> <div class="col-sm-3"> <input type="email" class="form-control" ng-model="person.email" name="email" id="email" required> <div class="validation-error" ng-show="(exampleForm.email.$dirty || invalidSubmitAttempt)&& exampleForm.email.$error.required"> Email is required. </div> <div class="validation-error" ng-show="(exampleForm.email.$dirty || invalidSubmitAttempt) && exampleForm.email.$error.email"> Email must be a valid email address. </div> </div> </div> <div class="form-group"> <label for="firstName" class="col-sm-2 control-label"> First Name * </label> <div class="col-sm-3"> <input type="text" class="form-control" ng-model="person.firstName" name="firstName" id="firstName" required ng-minlength="2" ng-maxlength="30"> <div class="validation-error" ng-show="(exampleForm.firstName.$dirty || invalidSubmitAttempt) && exampleForm.firstName.$error.required"> First Name is required. </div> <div class="validation-error" ng-show="(exampleForm.firstName.$dirty || invalidSubmitAttempt) && exampleForm.firstName.$error.maxlength"> First name must be less than 30 characters. </div> <div class="validation-error" ng-show="(exampleForm.firstName.$dirty || invalidSubmitAttempt) && exampleForm.firstName.$error.minlength"> First Name must be greater than 2 characters. </div> </div> </div> <div class="form-group"> <label for="lastName" class="col-sm-2 control-label"> Last Name Custom * </label> <div class="col-sm-3"> <input type="text" class="form-control" ng-model="person.lastName" name="lastName" id="lastName" required ng-minlength="2" ng-maxlength="30"> <i ng-show="exampleForm.lastName.$invalid" class="fa fa-exclamation-circle fa-lg"></i> <div class="validation-error" ng-show="(exampleForm.lastName.$dirty || invalidSubmitAttempt) && exampleForm.lastName.$error.required"> Last Name is required. </div> <div class="validation-error" ng-show="(exampleForm.lastName.$dirty || invalidSubmitAttempt) && exampleForm.lastName.$error.maxlength"> Last Name must be less than 30 characters. </div> <div class="validation-error" ng-show="(exampleForm.lastName.$dirty || invalidSubmitAttempt) && exampleForm.lastName.$error.minlength"> Last Name must be greater than 2 characters. </div> </div> </div> <div class="form-group"> <label for="favoriteNumber" class="col-sm-2 control-label"> Favorite Number </label> <div class="col-sm-3"> <input type="number" required class="form-control" ng-model="person.favoriteNumber" name="favoriteNumber" id="favoriteNumber"> <div class="validation-error" ng-show="(exampleForm.favoriteNumber.$dirty || invalidSubmitAttempt) && exampleForm.favoriteNumber.$error.number"> Favorite Number must be a number </div> </div> </div> <div class="form-group"> <label for="age" class="col-sm-2 control-label"> Age * </label> <div class="col-sm-3"> <input type="number" class="form-control" ng-model="person.age" name="age" id="age" min="0" max="140" required> <div class="validation-error" ng-show="(exampleForm.age.$dirty || invalidSubmitAttempt) && exampleForm.age.$error.number"> Age must be a number </div> <div class="validation-error" ng-show="(exampleForm.age.$dirty || invalidSubmitAttempt) && exampleForm.age.$error.min"> Age must be at least 0. </div> <div class="validation-error" ng-show="(exampleForm.age.$dirty || invalidSubmitAttempt) && exampleForm.age.$error.max"> Age must be at most 140. </div> <div class="validation-error" ng-show="(exampleForm.age.$dirty || invalidSubmitAttempt) && exampleForm.age.$error.required"> Age is required. </div> </div> </div> <div class="form-group"> <label for="favoriteLetter" class="col-sm-2 control-label"> Favorite Letter * </label> <div class="col-sm-3"> <input type="text" class="form-control" ng-model="person.favoriteLetter" name="favoriteLetter" id="favoriteLetter" maxlength="1" ng-pattern="/^[A-Za-z]$/" required> <div class="validation-error" ng-show="(exampleForm.favoriteLetter.$dirty || invalidSubmitAttempt) && exampleForm.favoriteLetter.$error.required"> Favorite Letter is required. </div> <div class="validation-error" ng-show="(exampleForm.favoriteLetter.$dirty || invalidSubmitAttempt) && exampleForm.favoriteLetter.$error.pattern"> Favorite Letter is invalid. </div> </div> </div> <div class="form-group"> <label for="favoriteWebsite" class="col-sm-2 control-label"> Favorite Website * </label> <div class="col-sm-3"> <input type="text" class="form-control" ng-model="person.favoriteWebsite" name="favoriteWebsite" id="favoriteWebsite" required> <div class="validation-error" ng-show="(exampleForm.favoriteWebsite.$dirty || invalidSubmitAttempt) && exampleForm.favoriteWebsite.$error.required"> Favorite Website is required. </div> <div class="validation-error" ng-show="(exampleForm.favoriteWebsite.$dirty || invalidSubmitAttempt) && exampleForm.favoriteWebsite.$error.url"> Favorite Website must be a valid url. </div> </div> </div> <div class="form-group"> <label for="favoriteWebsite" class="col-sm-2 control-label"> Favorite Website * </label> <div class="col-sm-3"> <select ng-model="person.favoriteColor" ng-options="c.name for c in colors" required> <option value="">Select...</option> </select> <div class="validation-error" ng-show="(exampleForm.favoriteColor.$dirty || invalidSubmitAttempt) && exampleForm.favoriteColor.$error.required"> Favorite Website is required. </div> </div> </div> <div class="form-group"> <label class="col-sm-2 control-label">Gender *</label> <div class="col-sm-1"> <label class="radio-inline"> <input type="radio" name="gender" required aa-val-msg ng-model="person.gender" value="male"> Male </label> <label class="radio-inline"> <input type="radio" name="gender" required ng-model="person.gender" value="female"> Female </label> </div> </div> <div class="form-group"> <label class="col-sm-2 control-label">Gender *</label> <div class="col-sm-1"> <label class="radio-inline"> <input type="radio" name="gender" required ng-model="person.gender" value="male"> Male </label> <label class="radio-inline"> <input type="radio" name="gender" required ng-model="person.gender" value="female"> Female </label> <div class="validation-error" ng-show="(exampleForm.gender.$dirty || invalidSubmitAttempt) && exampleForm.gender.$error.required"> Gender is required. </div> </div> </div> </div>
Some problems
- The markup above is very verbose and without careful study I would say it obfuscates what you are doing. This is just a form with validation folks!
- The DRY principle has completely died, almost everything looks “the same but a little different” and very boilerplate like
- Validation messages are tedious
- If you ever want to change from Twitter Bootstrap to something else (or heck even upgrade to a new version) you might be in for a serious brainless copy, paste, repeat nightmare
- We are wasting our bandwidth, the client’s bandwidth and possibly a slower UX as a result with this junk!
A better solution – AngularAgility Form Extensions
Now if you looked above remember this is just a simple form still with some validation and Angular bindings. Crazy how much HTML and directive markup is required to pull that off. Imagine having an entire application filled with those forms… ick.
Here is the same form from above implemented with AngularAgility Form Extensions:
<div ng-form="exampleForm" class="form-horizontal"> <input type="email" aa-field-group="person.email" required/> <input aa-field-group="person.firstName" required ng-minlength="2" ng-maxlength="30"/> <input aa-field-group="person.lastName" aa-label="Last Name Custom" required ng-minlength="2" ng-maxlength="30"/> <input type="number" aa-field-group="person.favoriteNumber"> <input type="number" aa-field-group="person.age" min="0" max="140" required> <input aa-field-group="person.favoriteLetter" maxlength="1" ng-pattern="/^[A-Za-z]$/" required> <input type="url" aa-field-group="person.favoriteWebsite" required> <select aa-field-group="person.favoriteColor" ng-options="c.name for c in colors" required> <option value="">Select...</option> </select> <div class="form-group"> <label class="col-sm-2 control-label">Gender *</label> <div class="col-sm-1"> <label class="radio-inline"> <input type="radio" name="gender" required aa-val-msg ng-model="person.gender" value="male"> Male </label> <label class="radio-inline"> <input type="radio" name="gender" required ng-model="person.gender" value="female"> Female </label> </div> </div> </div>
Wow, 159 lines down to 31 (and it’s totally flexible)
Already convinced? Here is a live demo, the source and the API Docs
The goal of Form Extensions is the distill HTML, Angular and Bootstrap (or whatever you want it’s completely customizable with a provider model) to the bare minimum possible. Here are some of the goals of the project that are depicted above:
- Offer a drastic reduction in the amount of boring, boilerplate, repetitive, error-prone HTML required to produce forms, labels and validation messages.
- Automatically generate Angular.js fields for use in form validation, their error messages AND labels (overridably of course)
- On blur and on invalid submit attempt showing of validation messages.
- Form extensions programatically extends forms at myForm.$aaFormExtensions = {…}
- Code is cleaner and easier to read. Form Extensions is DSL that distills your HTML down to only what is required.
- Feel free to use full blown markup whenever you want complete control.
- Mix and match the directive components of Form Extensions to get exactly what you’d like for each situation.
- It does exactly what you want: Everything is overridable on a global and per-instance basis through a rich provider model.
HTML size reduction comparison
Before Form Extensions
After Form Extensions
Conclusion
Save some typing, make your code cleaner, save bandwidth, have more fun! Whatever the thought give the library a shot and let me know what you think. Myself and others have been using it on various projects and have seen huge increases in productivity. Perhaps my favorite part about it is I get to focus on the fun aspects of coding not the mundane repetitive stuff that Form Extensions eliminates!
Great article! I’ve been searching for easier way to make validation less-painful in angular, was going to make directives for it, then found AngularAgility, I’ll try this
Pretty cool set of directives!!! Thanks!
However we try to use it and have some issues with validation of hidden fields.
Say one needs to conditionally validate one field based on value of another.
In our case that field is also hidden. It looks like AA still validates that hidden field.
Any suggestions how one may work around?
Also is there any way to get called back before valid form submitted? We sort of need server side validation involving multiple fields.
Thanks
Hi, Is there some way to do a global config, by example. aa-col, aa-lbl-col, and translate messages? Great Job! Thanks!