angular-bootstrap-show-errors icon indicating copy to clipboard operation
angular-bootstrap-show-errors copied to clipboard

Does not work with ngSwitch inside ngRepeat

Open timmipetit opened this issue 10 years ago • 9 comments

I tried using the fix from #16, but unfortunately it doesn't work for me. What I have is something that looks like:

<div ng-repeat="field in fields" class="row form-group" show-errors>
  <div class="col-md-8" ng-switch on="field.type">
    <select ng-switch-when="select" name="form{{$index}}" class="form-control"></select>
    <input type="number" ng-switch-when="number" name="form{{$index}}" class="form-control">
    <input type="text" ng-switch-when="text" name="form{{$index}}" class="form-control">
  </div>
</div>

This results in the error: show-errors element has no child input elements with a 'name' attribute and a 'form-control' class

However, if I change the code to:

<div ng-repeat="field in fields" class="row form-group" show-errors>
  <div class="col-md-8">
    <input type="text" name="form{{$index}}" class="form-control">
  </div>
</div>

Everything works as expected.

timmipetit avatar Nov 28 '14 12:11 timmipetit

I had a similar problem (with ngMessages) and wrote a simpler version of the directive for my own needs: http://plnkr.co/edit/LzlcxTXwZNogwCznPdIE?p=preview It should handle ngSwitch and similar without problem.

VanTanev avatar Dec 15 '14 00:12 VanTanev

Thanks @VanTanev, your version seems to be working in our case as well!

timmipetit avatar Dec 15 '14 10:12 timmipetit

I'm having the same issues. I will take a look at @VanTanev version, thanks

gommo avatar May 03 '15 10:05 gommo

@gommo I just went back to my code and updated it a little. In a real project, it turns out that you often times need to put multiple .form-control input elements inside a single div.

This update makes the directive detect errors on multiple elements: http://plnkr.co/edit/a6XWR657EpTOW2JL7bhM?p=preview


And now, for some extra. In my projects, I do backend validation on all forms. Sometimes, there are validations that only the backend can do. In this case, I need to mark a .form-group as having an error based on the errors returned from the backend. In my case, those have the form of:

var user = {
  "username": "VanTanev",
  "password": "123",
  "errors": {
    "username": ["username is already in use"]
  }
}

So, in order to make the showErrors directive show errors for this as well, I made the following changes:

<!-- the important part --> <div show-errors="user"> <!-- the important part -->

  <input type="text" name="username" class="form-control" ng-model="user.username" />
  <input type="text" name="password" class="form-control" ng-model="user.password" />
</div>

and modified the watch functuon in the directive:

$scope.$watch(function() {
  // on scope changes, check if the input's validation status has changed.
  // additionally, if an input has not been touched or the form has not been
  // yet submitted, consider the element valid
  var hasFormError = ngFormCtrl[childInputName].$invalid
        && (ngFormCtrl[childInputName].$touched || ngFormCtrl[childInputName].$dirty || ngFormCtrl.$submitted);

  // additionally check the model for errors on this field
  var hasModelError = !!($attrs['showErrors'] && $scope.$eval($attrs['showErrors']  + '.errors["' + childInputName + '"].length'));

  return hasFormError || hasModelError;
}, function(is_invalid) {
  $element.toggleClass('has-error', is_invalid);
}); 

Basically, I give the model object's name to the showErrors directive by setting it as an attribute on the div, and can then access that name with $attrs['showErrors']. Afterwards, I check for model errors by evaluating !!user.errors["username"].length - if this is true, the user object has validation errors from the backend and the field must be marked with .has-error class.

VanTanev avatar May 03 '15 11:05 VanTanev

Great work ! @VanTanev Do you wanna put it on your github ?

maxisam avatar Jul 24 '15 17:07 maxisam

So basically the real magic here is the timeout right? I came here because I had a similar issue where the show-errors didnt see an input generated several levels of components/directives deep down my DOM nest.

VictorioBerra avatar Jun 21 '17 14:06 VictorioBerra

@VictorioBerra For the most part, yes. This is the current version that I use in my projects: https://gist.github.com/VanTanev/724cef9f2950cb2b246e33bd407187e0

import _ from 'lodash'

/* @ngInject */
export default function highlightErrorsDirective($timeout) {
    var directive = {
        restrict: 'A',
        require: '^form',
        link: highlightErrorsLinkFn,
    };

    return directive;

    ////////////////////////

    function highlightErrorsLinkFn(scope, element, attr, ngFormCtrl) {
        // This timeout is necessary to handle the case when we have
        // ng generated code inside of the highlight-errors element.

        // We want to allow interpolation of child elements, so we just
        // offset the attachment of the watchers until the browser renders
        // the next frame, by using a $timeout(func, 0, false)
        let promise = $timeout(function() {
            // we search for .form-control where the name attriubte is set
            var childInputs = element[0].querySelectorAll('.form-control[name]');

            // and add watchers for all elements
            _.each(childInputs, function(childInput) {
                var childInputName = childInput && childInput.getAttribute('name');

                if (!childInputName || angular.isUndefined(ngFormCtrl[childInputName])) {
                    // if we cannot find the expected element, throw an error and
                    // halt compilation
                    throw new Error(`highlightErrors directive requires that you have
                        a child ".form-control" element with a "name" attribute.`);
                }

                scope.$watch(function() {
                    // on scope changes, check if the input's validation status has changed.
                    // additionally, if an input has not been touched or the form has not been
                    // yet submitted, consider the element valid
                    var hasFormError = ngFormCtrl[childInputName].$invalid &&
                        (ngFormCtrl[childInputName].$touched ||
                         ngFormCtrl[childInputName].$dirty ||
                         ngFormCtrl.$submitted);

                    var hasModelError = false;
                    if (attr.highlightErrors) {
                        hasModelError = !!scope.$eval(
                            `${attr.highlightErrors}.errors["${childInputName}"].length`
                        );
                    }

                    return hasFormError || hasModelError;
                }, function(hasError) {
                    element.toggleClass('has-error', hasError);
                });
            });
        }, 0, false);

        scope.$on('$destroy', () => $timeout.cancel(promise))
    }
}

All the best :)

VanTanev avatar Jun 21 '17 17:06 VanTanev

@VanTanev I hope im not asking too much, but could you create a repo for this and add a license? maybe MIT? That way we can all contribute and use this in projects at work, etc.

VictorioBerra avatar Jun 21 '17 17:06 VictorioBerra

I will add the MIT license to the gist, and see about making a dedicated repo; Would need to setup tests, builds, docs, etc etc etc for that.

VanTanev avatar Jun 21 '17 17:06 VanTanev