ProfK ProfK - 6 months ago 119
AngularJS Question

ViewModel-based validation with Angular

I would like to have a JavaScript view model object that contains validation rules, similar to ASP.NET MVC view models with data annotations, that I can bind to Angular views. Then, I would e.g. like to call a

Validate
method on that object at certain stages in its lifetime before posting it to a server.

This would allow for me to stay close to an MVVM-like approach, where the UI is quite light and not sooo smart, and the view model, whatever view model, that is represented by a view, need to require changes to the view markup in order to change validation rules.

This way I can even possibly leverage MVC's data annotations to build a serializable view model, with rules, on the server, and return that model and all it's validations etc. to the client as JSON.

How could I go about achieving this type of validation in Angular, vs. the much more common, per element/model property validation achieved by means of directives?

Answer

I have implemented something that might help you on the most recent project I am working on. We use AngularJS on the front-end and ASP.NET WEB API on the back-end. All of the HTML forms are generated automatically based on the properties and data annotations contained in my POCO classes.

On the server-side I have entities and DTO's. My entities contain the database specific annotations and DTO's contain my view specific annotations. I will give a brief example showing one property in one class and how I render the UI for this. Here are the server-side objects:

public class Discount
{
    [StringLength(40)]
    [Required]
    public String Name { get; set; } 
}

public class DiscountDto : IDto<Discount>
{
    [Display(ResourceType = typeof(ApplicationStrings), Name = "Name", ShortName = "Name_Placeholder")]
    [UI(Row = 1, Width = 6)]
    public String Name { get; set; }
}

This property gets rendered on the UI like so:

<div class="form-group">
  <label class="col-sm-2 control-label"> Name: </label> 
  <div class="col-sm-6"> 
    <input class="form-control" ng-model="model[options.key]"  required="required" maxlength="40" placeholder="Enter the name...">
  </div>
</div>

The <input /> field has the required, placeholder and maxlength properties auto set. The HTML label, bootstrap column widths are also auto set based on the custom UI data annotation. Row = 1 means to to display this field first in the form and Width = 6 means the field should take up a column width of 6: class="col-sm-6". The label text and placeholder text are pulled from Resource files. If this is what you are looking for then read on :-)

I have created a Controller MetaController that takes in the name of the DTO as a parameter: api/Meta/DiscountDTO for example. This controller simply loops through all the properties on the DTO object and the associated entity and pulls out the data annotations, transforms them into a FormMetadata class and returns a List<FormMetadata> to the client. The FormMetadata class just contains properties like IsRequired, IsDisplayed, IsReadonly, etc. just to turn the annotations into something more readable for the front-end developers. Here is a snippet from the MetaController:

var type = Type.GetType("<DTO_goes_here>");
List<FormMetadata> formMetadata = new List<FormMetadata>();

foreach (var prop in type.GetProperties())
{
    var metadata = new FormMetadata();
    metadata.Key = prop.Name.ToLower().Substring(0, 1) + prop.Name.Substring(1, prop.Name.Length - 1);
    metadata.Type = prop.PropertyType.FullName;

    object[] attrs = prop.GetCustomAttributes(true);

    foreach (Attribute attr in attrs)
    {
        if (attr is RequiredAttribute)
        {
            metadata.IsRequired = true;
        }
        else if (attr is StringLengthAttribute)
        {
            var sla = (attr as StringLengthAttribute);
            metadata.MinLength = sla.MinimumLength; 
            metadata.MaxLength = sla.MaximumLength;
        }
        // etc.
    }

    formMetadata.Add(metadata);
}

This endpoint would return the following JSON for the Name property:

{  
   "$id":"3",
   "key":"name",
   "display":"Name",
   "type":"System.String",
   "placeholder":"Enter the name...",
   "isRequired":true,
   "isEditable":true,
   "isDisplayed":true,
   "isReadonly":false,
   "displayInList":true,
   "width":6,
   "row":1,
   "col":0,
   "order":0,
   "maxLength":40,
   "minLength":0,
   "lookup":null,
   "displayAs":null
}

On the client side I have created a custom Angular directive <entity-form /> that takes in the name of the DTO as a parameter like so:

<entity-form entity-type="DiscountDTO"></entity-form>. This directive will then call the MetaController to get the validation rules for the Discount entity and render the form based on the rules returned. To render the form I use an awesome library called angular-formly. This library allows to create forms from javascript without writing any HTML. I won't get in to too much detail about angular-formly here but you basically create a Javascript object with the details of the form you want to render and pass it into an angular-formly directive and it takes care of rendering the form for you. This is a basic example of the type of object you pass to angular-formly to render an <input /> box with a label of "Text":

{
  "key": "text",
  "type": "input",
  "templateOptions": {
    "label": "Text",
    "placeholder": "Type here to see the other field become enabled..."
  }
}

So, I basically take the metadata returned from the MetaController and build up an object that angular-formly understands and pass it into the angular-formly directive and it renders the form for me. I know this answer could have been a LOT longer with more examples, etc. but I felt this was a lot to read through as is. I hope this gives you enough info.

I would love to make this more generic and open-source it - if anyone is interested in working on something like this let me know :-)

Comments