Saturday, 23 May 2015

Angular UI Bootstrap Datepicker Weirdness

The Angular UI Bootstrap Datepicker is fan-dabby-dozy. But it has a ... pecularity. You can use the picker like this:


<div ng-app="peskyDatepicker">
    <div ng-controller="DatepickerDemoCtrl as vm">
        <input type="text" class="form-control"
               datepicker-popup="mediumDate"
               is-open="vm.valuationDatePickerIsOpen"
               ng-click="vm.valuationDatePickerOpen()"
               ng-model="vm.valuationDate" />
    </div>
</div>


angular
.module('peskyDatepicker', ['ui.bootstrap'])
.controller('DatepickerDemoCtrl', [
function () {
  
  var vm = this;
  
  vm.valuationDate = new Date();
  vm.valuationDatePickerIsOpen = false;
  
  vm.valuationDatePickerOpen = function () {

      this.valuationDatePickerIsOpen = true;
  };
}]);

The above code produces a textbox which, when clicked upon, renders the datepicker popup (which vanishes upon date selection). This works because the ng-click directive calls the valuationDatePickerOpen function on the controller which sets the valuationDatePickerIsOpen property to be true and that property happens to be bound to the is-open attribute. Your knee bone connected to your thigh bone, Your thigh bone connected to your hip bone... This makes sense. This works. Great.

But I want something a little prettier - I want to use the lovely calendar glyph to trigger the datepicker popup like in the docs. That should be really easy right? I just tweak the HTML to add a calendar button and the associated ng-click="vm.valuationDatePickerOpen()":


<div ng-app="peskyDatepicker">
    <div ng-controller="DatepickerDemoCtrl as vm">
      <p class="input-group">
        <input type="text" class="form-control" 
               datepicker-popup="mediumDate" 
               is-open="vm.valuationDatePickerIsOpen" 
               ng-click="vm.valuationDatePickerOpen()" 
               ng-model="vm.valuationDate" />
        <span class="input-group-btn">
          <button type="button" class="btn btn-default" 
                  ng-click="vm.valuationDatePickerOpen()">
            <i class="glyphicon glyphicon-calendar"></i>
          </button>
        </span>
      </p>
    </div>
</div>

Miraculously, this doesn't work. Which is strange - I mean it ought to... The same ng-click directive is sat on our new calendar button as is in place on the datepicker itself. So what's happening? Well let's do some investigation. If you take a look at the docs you'll see that their example with the calendar glyph is subtly different to our own. Namely, when the opener function is invoked, the official docs pass along $event. To what end? Well, the docs opener function does something that our own does not. This:


  $scope.open = function($event) {
    $event.preventDefault();
    $event.stopPropagation();

    $scope.opened = true;
  };

Ignore all the $scope malarkey - I want you to pay attention to what is happening with $event. preventDefault and stopPropogation are being called. This is probably relevant.

I decided to do a little experimentation. I created a Plunk which demonstrates the datepicker and uses $watch to track what happens to valuationDatePickerIsOpen. The Plunk featured 2 calendar glyphs - the left one doesn't pass along $event to valuationDatePickerOpen when it is clicked and the right one does. When $event is passed we call preventDefault and stopPropogation.

After a little experimentation of my own I discovered that calling $event.stopPropogation() is the magic bullet. Without that in place valuationDatePickerIsOpen gets set to true and then immediately back to false again. I do not know why. There may be an entirely sane reason for this - if so then please do post a comment and let me know. It wouldn't hurt for the Angular UI Bootstrap Datepicker docs to mention this. Perhaps it's time to submit a PR....

Monday, 11 May 2015

NgValidationFor Baby Steps

I thought as I start the NgValidationFor project I'd journal my progress. I'm writing this with someone particular in mind: me. Specifically, me in 2 years who will no doubt wonder why I made some of the choices I did. Everyone else, move along now - nothing to see. Unless the inner workings of someone else's mind are interesting to you... In which case: welcome!

Getting up and running

I've got a project on GitHub and I'm starting to think about implementations. One thing that bit me on jVUN was being tied to a specific version of ASP.Net MVC. For each major release of ASP.Net MVC I needed separate builds / NuGet packages and the like. A pain. Particularly when it came to bug fixes for prior versions - the breaking changes with each version of MVC meant far more work was required when it came to shipping fixes for MVC 4 / MVC 3.

So with that in mind I'm going to try and limit my dependencies. I'm not saying I will never depend upon ASP.Net MVC - I may if I think it becomes useful to give the users a nicer API or if there's another compelling reason. But to start with I'm just going to focus on the translation of data annotations to Angular validation directive attributes.

To that end I'm going to begin with just a class library and an associated test project. I'm going to try and minimise the dependencies that NgValidationFor has. At least initially I may even see if I can sensibly avoid depending on System.Web (mindful of the upcoming ASP.Net 5 changes). Let's see.

A little time passes.......

So what have we got?

My first efforts have resulted in the implementation of the RequiredAttribute. This is the code right now. It's made up of:

  1. NgValidationFor.Core - the core part of the project which converts data annotations into AngularJS 1.x validation directive attributes.
  2. NgValidationFor.Core.UnitTests - the unit tests for the core
  3. NgValidationFor.Documentation - this is an ASP.Net MVC project which will become a documentation site for NgValidationFor. It also doubles as a way for me to try out NgValidationFor.
  4. NgValidationFor.Documentation.UnitTests - unit tests for the documentation (there's none yet as I'm still spiking - but when I'm a little clearer, they will be)

How can it be used? Well fairly easily. Take this simple model:


using System.ComponentModel.DataAnnotations;

namespace NgValidationFor.Documentation.Models
{
    public class RequiredDemoModel
    {
        [Required]
        public string RequiredField { get; set; }
    }
}

When used in an MVC View for which RequiredDemoModel is the Model, NgValiditionFor can be used thusly:


@using NgValidationFor.Core
@using NgValidationFor.Documentation.Models
@model RequiredDemoModel
<input type="text" 
       name="userName" 
       ng-model="user.name"
       @Html.Raw(Model.GetAttributes(x => Model.RequiredField))
       >

Which results in this HTML:


<input type="text"
       name="userName" 
       ng-model="user.name"
       required="required"
       >

Tada!!!! It works.

So what now?

Yes it works, but I'm not going to pretend it's pretty. I don't like having to wrap the usage of NgValidationFor with Html.Raw(...). I'm having to do that because GetAttributes returns a string. This string is then HTML encoded by MVC. To avoid my quotation marks turning into &quot; I need to actually be exposing an IHtmlString. So I'm going to need to depend upon System.Web. That's not so bad - at least I'm not tied to a specific MVC version.

I'm not too keen on the implementation I've come up with for NgValidationFor either. It's a single static method at the minute which does everything. It breaks the Single Responsibility Priniciple and the Open/Closed Principle. I need to take a look at that - I want people to be able to extend this and I need to think about a good and simple way to achieve that.

Finally, usage. Model.GetAttributes(x => Model.RequiredField) feels wrong to me. I think I'm happy with having this used as an extension method but it needs to be clearer what's happening. Perhaps Model.NgValidationFor(x => Model.RequiredField) would be better. I need to try a few things out and come up with a nicer way to use NgValidationFor.

Tuesday, 5 May 2015

A tale of Angular, html5mode, ASP.Net MVC and ASP.Net Web API

So. You want to kick hash based routing to the kerb. You want real URLs. You've read the HTML5 mode section of the Angular $location docs and you're good to go. It's just a matter of dropping $locationProvider.html5Mode(true) into your app initialisation right?

Wrong.

You want your URLs to be shareable. If, when you copy the URL out of your browser and send it someone else, they do not get taken to the same position in the application as you do then I've got news for you: THAT'S NOT REALLY A URL. And just using $locationProvider.html5Mode(true) has done nothing useful for you. You want to ensure that, if the URL entered in the browser does not relate to a specific server-side end-point, the self-same HTML root page is always served up. Then Angular can load the correct resources for the URL you have entered and get you to the required state.

There are tips to be found in Angular UI's How to: Configure your server to work with html5Mode doc. However they required a little extra fiddling to get my ASP.Net back end working quite as I wanted. To save you pain, here are my cultural learnings.

ASP.Net MVC

I had an ASP.Net MVC app which I wanted to use html5mode with. To do this is simply a matter of tweaking your RouteConfig.cs like so:


    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            // Here go the routes that you still want to be able to hit
            routes.MapRoute(
                name: "IAmARouteThatYouStillWantToHit",
                url: "ThatsWhyIAmRegisteredFirst",
                defaults: new { controller = "Hittable", action = "Index" }
            );

            // Everything else will hit Home/Index which serves up the root angular app page
            routes.MapRoute(
                name: "Default",
                url: "{*anything}", // THIS IS THE MAGIC!!!!
                defaults: new { controller = "Home", action = "Index" }
            );
        }

With this in place my existing routes work just as I would hope. Any route that doesn't fit that registered can be assumed to be html5mode related and will serve up the root angular app page as I'd hope.

ASP.Net Web API

Later I realised that the app in question was mostly static content. Certainly the root angular app page was and so it seemed wasteful to require an ASP.Net MVC controller to serve up that static content. So I stripped out MVC from the app entirely, choosing to serve raw HTML instead. For the dynamic parts I switched to using Web API. This was "hittable" as long as I had my WebApiConfig.cs and my system.webServer section in my web.config lined up correctly, viz:


    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API routes
            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

            // other stuff
        }
    }


<configuration>

    <system.webServer>

        <defaultDocument>
            <files>
                <clear />
                <add value="build/index.html" /> <!-- This is the root document for the Angular app -->
            </files>
        </defaultDocument>
        
        <rewrite>
            <rules>
                <rule name="Main Rule" stopProcessing="true">
                    <match url=".*" />
                    <conditions logicalGrouping="MatchAll">
                        <!-- Allows "api/" prefixed URLs to still hit Web API controllers 
                             as defined in WebApiConfig -->
                        <add input="{REQUEST_URI}" pattern="api/" ignoreCase="true" negate="true" />
                        
                        <!-- Static files and directories can be served so partials etc can be loaded -->
                        <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
                        <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
                    </conditions>
                    <action type="Rewrite" url="/" />
                </rule>
            </rules>
        </rewrite>
        
    </system.webServer>

</configuration>

With this in place I can happily hit "api" prefixed URLs and still land on my Web API controllers whilst other URLs will serve up the root angular app page. Lovely.