Angular Formly Error Summary when using fieldGroups

1.9k Views Asked by At

Hopefully this will be a more generic Angular JS question rather than something specific to Angular Formly.

I've been following the framework provided here for building an error summary on the angular formly form. All works well.....but!

In the example, their model is as follows:

vm.fields = [
  {
    key: 'picky',
    type: 'customInput',
    templateOptions: {
      label: 'Picky field...',
      placeholder: 'This is required and has a maxlength of 5 and minlength of 3',
      required: true,
      maxlength: 5,
      minlength: 3
    }
  },
  .....
  {
    key: 'ip',
    type: 'customInput',
    validators: {
      ipAddress: {
        expression: function(viewValue, modelValue) {
          var value = modelValue || viewValue;
          return /(\d{1,3}\.){3}\d{1,3}/.test(value);
        },
        message: '$viewValue + " is not a valid IP Address"'
      }
    },
    templateOptions: {
      label: 'IP Address',
      required: true,
      type: 'text',
      placeholder: '127.0.0.1',
    }
  }
];

Then, if we look at the HTML, we can see that these fields are being passed into the error summary as such:

<formly-error-summary form="vm.form" fields="vm.fields"></formly-error-summary>

For simple form structure, this works fine, but, if you want to use a Bootstrap layout, as described here then your model ends up looking something mine does:

vm.rentalFields = [
{
    template: '<div class="row"><div class="col-xs-12"><h3>About You</h3></div></div>'  
},
{
    className: 'row',
    fieldGroup: [
    {
        className: 'col-xs-6',
        type: 'customInput',
        key: 'first_name',
        templateOptions: {
            type: 'text',
            label: 'First Name',
            placeholder: 'Enter your first name',
            required: true
        }
    },
    {
        className: 'col-xs-6',
        type: 'customInput',
        key: 'last_name',
        templateOptions: {
            type: 'text',
            label: 'Last Name',
            placeholder: 'Enter your last name',
            required: true
        },
        expressionProperties: {
            'templateOptions.disabled': '!model.first_name'
        }   
    }
    ]
},
{
    template: '<div class="row"><div class="col-xs-12"><h3>License and Insurance Details</h3></div></div>',
    hideExpression: '!model.email'
}
.....

Now, when we pass in vm.rentalFields to the error summary, instead of accessing the fields, it instead just validates each object. I can get round this by doing something like:

<formly-error-summary form="vm.rentalForm" fields="vm.rentalFields[1].fieldGroup"></formly-error-summary>

This of course is not ideal since there will be fields in other field groups that I will want to validate, for proving the issue though it's fine for now. I have tried just passing in 'vm.rentalFields.fieldGroup' but as I suspected, that returns nothing.

So, is there a way I can recursively pass in all the fieldGroups within the vm.rentalField object or is this something that I should handle within the code of the Directive itself.

angular.module("formlyApp").directive('formlyErrorSummary', function() {
    return {
      scope: {},
      bindToController: {
        form: '=',
        fields: '='
      },
      templateUrl: 'js/Directives/formly-error-summary.html',
      controllerAs: 'vm',
      controller: function() {
        var vm = this;

        vm.getErrorAsList = getErrorAsList;
        console.log(vm.fields);
        function getErrorAsList(field) {
          return Object.keys(field.formControl.$error).map(function(error) {
            // note, this only works because the customInput type we have     defined.
            return field.data.getValidationMessage(error);
          }).join(', ');
        }
      }
};
});

EDIT

Ok, so, after taking advice from Ken below, I have been able to modify my formlyErrorSummary directive so that it now is at least able to get the errors for the model. This has numerous issues in it since the $scope.$watch is doing a deep level comparison and even on the first page load, the whole thing is fired 3 times! I've added in some rudimentary escapes to try and combat this and for now at least I have the errors, the next issue I have is within the HTML where I am call ng-repeat="field in vm.fields" which is effectively the same issue, so how would I work round this? Part of me is thinking of some anonymous object that would hold the fields message and whether or not is is valid and then parse that inside the HTML, but I'm not sure if this way of thinking is applicable to Angular?

controller: function($scope) {

var vm = this;

$scope.$watch('vm.fields', function(){
    for(var i = 0; i < vm.fields.length; i++)
        if(vm.fields[i].fieldGroup) {
            for(var j = 0; j < vm.fields[i].fieldGroup.length; j ++)
                if(vm.fields[i].fieldGroup[j].formControl) {
                    var err = getErrorAsList(vm.fields[i].fieldGroup[j]); 
                    if(err)
                        vm.getErrorAsList = err;
                }
        }
}, true);

SOLUTION - POSSIBLY

After much hacking around, I think I finally have this working so that the error messages are now displayed both inline and in a summary at the top.

My final directive function now creates an array each time it is run which will hold all of the error messages, it has to be flushed within the $watch otherwise when the field is valid, the error message will persist within the array so we simply rebuild the entire thing each time.....given I'm already using a deep level watch here, I'm hoping any performance hits will be negligible.

vm.errs = [];

$scope.$watch('vm.fields', function(){
    vm.errs = [];
    for(var i = 0; i < vm.fields.length; i++)
        if(vm.fields[i].fieldGroup) {
            for(var j = 0; j < vm.fields[i].fieldGroup.length; j ++)
                if(vm.fields[i].fieldGroup[j].formControl) {
                    var err = getErrorAsList(vm.fields[i].fieldGroup[j]); 
                    if(err)
                        if(vm.errs.indexOf(err) === -1)
                          vm.errs.push(err);
                }
        }
}, true);  

Then, within the directives template, I had to remove the vm.fields reference as that was obviously not going to work in this approach. Since I knew that this summary would only be shown if the form was invlaid, I could remove so of the other checks that were being carried out and eventually ended up with the following HTML:

<div class="row">
    <div class="col-xs-12">
        <div class="formly-error-summary bg-danger" ng-if="vm.form.$invalid">
            <div ng-repeat="err in vm.errs" class="color-error">
                <i class="glyphicon glyphicon-remove"></i>
                <span>
                    {{err}}
                </span>
            </div>
        </div>
    </div>
</div>

I'm still not 100% happy with this, it gets the job done, but I'm not sure if it's the 'Angular' way of doing this and the fact I'm using $scope.$watch on the fields object is a little bit annoying to my developer OCD, but a solution it is all the same.

If anyone has any refinements or suggestions for improvements to this let me know please, still getting to grips with Angular but this has been a pretty fun learning experience!

0

There are 0 best solutions below