1

I'm attempting to wire up an adapter to enable the markup that ASP.Net MVC emits for client-side validation to work within AngularJS, and I've encountered an interesting snag. If I dynamically add the required attribute via a directive compile function:

var myApp = angular.module('myApp', []).directive('valRequired', function() {
    return {
        compile: function (element) {

            element.attr('required', 'required');

            return function() { /* other custom logic here */ }
        }
    };
});

The select element won't validate as required. It only appears to be a problem when dynamically adding the attribute (jsFiddle).

Clarification: I'd like to use MVC's @Html.TextBoxFor(...) as-is. For a DataAnnotations-based model, the data-val-* attributes it emits contain information on which validations to run and what the error messages should be. I'm not looking for assistance wiring up the error messages, I just need to be able to wire up a directive that tells the input, select, etc. to use the required validation.

FMM
  • 4,289
  • 1
  • 25
  • 44
  • 1
    like ng-required="{expression to test}" ? – TheSharpieOne Sep 05 '13 at 22:53
  • Not really. ASP.Net MVC emits the `data-val-required` attribute using things like `@Html.TextBoxFor(...)` and the like. I'd like to automatically transform them into their angularjs equivalents and add some wiring to plumb the MVC-emitted validation messages into the app as well. – FMM Sep 06 '13 at 01:41
  • Perhaps this will help: http://stackoverflow.com/questions/4844001/html5-data-with-asp-net-mvc-textboxfor-html-attributes Also, `ng-require` can be written as `data-ng-require` and will still be parsed by angular. – TheSharpieOne Sep 06 '13 at 02:51
  • If you're trying to get the angular equivalent of the MVC emitted attributes (like `data-val-required`) you can create your own `@Html` extension methods like `@Html.AngularTextBoxFor(...)` and save the client-side performance penalty. I've done this in my app. If you're interested I could write this up further at some point. – jandersen Sep 06 '13 at 05:14
  • @jandersen I understand where you're coming from. However, I'd rather not reinvent the wheel; MVC already does an excellent job of marking up the DOM with all the necessary information to do DataAnnotations-based validation. I'm just trying to wire those bits up with angularJS instead of jQuery validation. Making a custom `HtmlHelper` extension method still doesn't solve the entire problem: I also need to wire up the validation messages so that `@Html.ValidationMessageFor(...)` works properly as well. – FMM Sep 06 '13 at 13:33

2 Answers2

1

Let me start out with this isn't pretty, but it works. I tried different ways to get the native directive to work, but to no avail. It looks like by the time this directive executes it is too late.

This will look for your data-val-required attribute and add validation to the element.

It will trigger all of the same things so myForm.mySelect.$valid will still work as well as myForm.mySelect.$error.required

http://jsfiddle.net/TheSharpieOne/knc8p/

var myApp = angular.module('myApp', []).directive('valRequired', function () {
    return {
        require: 'ngModel',
        restrict: 'A',
        link: function (scope, elm, attr, ctrl) {
            if (!ctrl || !attr.valRequired) return;
            attr.required = true; // force truthy in case we are on non input element

            var validator = function (value) {
                if (attr.required && (value == '' || value === false)) {
                    ctrl.$setValidity('required', false);
                    return;
                } else {
                    ctrl.$setValidity('required', true);
                    return value;
                }
            };

            ctrl.$formatters.push(validator);
            ctrl.$parsers.unshift(validator);

            attr.$observe('required', function () {
                validator(ctrl.$viewValue);
            });
        }
    };
});


function MyCtrl($scope, $http) {
    $scope.model = {
        property: ''
    };
}
TheSharpieOne
  • 25,646
  • 9
  • 66
  • 78
  • This appears to be the only way to make it work, I agree. I did some code surfing in the AngularJS codebase on GitHub. It appears that the bit that hooks into the `ngModel` controller isn't accessible in a way that can be consumed outside of the `input` or `select` directives. – FMM Sep 18 '13 at 21:18
0

UPDATE I've thought of a better way to answer the question. The old answer is below the new one.


You can get a reference to AngularJS' required directive and apply it to your own. Heres a code sample that will do this:

var myApp = angular.module('myApp', []).directive('valRequired', function(requiredDirective) {
    var newDirective = {},
        angularDirective = requiredDirective[0]; //This assumes angular's required directive is the first one

    //Copy over all other properties of the angular directive
    angular.extend(newDirective, angularDirective);

    //Change the name of our directive back to valRequired
    newDirective.name = 'valRequired';

    //Provide our own logic in the linking function
    newDirective.link = function(scope, element, attr, ctrl){
        //Super call
        angularDirective.link.apply(this, arguments);
        if(attr.valRequired === 'required'){
            attr.$set('ngRequired', 'true');
        } else {
            attr.$set('ngRequired', 'false');
        }
    }

    return newDirective;
});

<input data-val-required="required" ng-model="foo" />

OLD ANSWER

Using jQuery's or jQLite's attr() method does not change AngularJs' Attributes object. The Attributes object is what directives use as values for their logic.

You will also need to include the ng-required attribute, although you will not need to bind any angular expressions to it. This question will help you out there: Html5 data-* with asp.net mvc TextboxFor html attributes

The reason for this is we need to force angular to apply the directive to this node. Updating the attribute after the compile phase of a template will not notify angular to apply new directives to the node.

This should work:

var myApp = angular.module('myApp', []).directive('valRequired', function() {
    return {
        priority : 101, //The priority needs to run higher than 100 to get around angularjs' default priority for ngRequired
        link: function (scope, element, attr) {

            if(attr.valRequired === 'true'){
                attr.$set('ngRequired', 'true');
            } else {
                attr.$set('ngRequired', 'false');
            }
        }
    };
});

<input ng-required data-val-required="true" ng-model="foo" />
Community
  • 1
  • 1
Clark Pan
  • 6,027
  • 1
  • 22
  • 18
  • I was not able to get this to work without manually adding `ng-required` to the tag, the question / question's comments mentioned this was not an option. – TheSharpieOne Sep 12 '13 at 13:55
  • The ng-required simply needs to be there, and will be switched on or off depending on the value of data-val-required. This is easily achievable with TextBoxFor. Updated answer to clarify this point – Clark Pan Sep 13 '13 at 03:50
  • What does `ng-repeat` have to do with anything? – FMM Sep 13 '13 at 17:56
  • the ng-required is needed so that the directive is applied. If ng-required is not there, angular would not know to apply the ngRequired directive to this input – Clark Pan Sep 14 '13 at 04:11
  • You mean `ng-required`, then, not `ng-repeat`? If so, never mind =) – FMM Sep 16 '13 at 18:37
  • Still, I can't accept this as the answer or award the bounty. Have you looked at the jsFiddle? – FMM Sep 17 '13 at 13:09
  • I've updated your fiddle to make it work : http://jsfiddle.net/zFfZF/5/ Changing the property of `data-val-required` will determine whether `ng-required` is applied or not. `ng-required` needs to be on the element regardless, otherwise the directive will not be applied at all. The value of `ng-required` does not need to change, the attribute just needs to be there. – Clark Pan Sep 18 '13 at 00:42
  • I've thought about my comment above a bit more and have updated my answer. – Clark Pan Sep 18 '13 at 01:19