Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 971
ProfK Avatar asked Oct 30 '15 04:10

ProfK


2 Answers

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 :-)

like image 132
keithm Avatar answered Nov 05 '22 13:11

keithm


According to MVC and MVVM with AngularJS, AngularJS follows MVVM design pattern.

I think you need something like Adding Unobtrusive Client Validation from the Microsoft .NET MVC Framework to Angular.

If you wanna create custom HTML helper for Razor, See Validation in Angular forms which has 2 parts.

I also found ngval which seems not to be stable.

like image 42
Amirhossein Mehrvarzi Avatar answered Nov 05 '22 13:11

Amirhossein Mehrvarzi