Search code examples
angularjsangularjs-directiveangularjs-controller

How to update a form controller in AngularJS after changing input constraints


I have been recently working on dynamically changing constraints on input fields on a form. The aim was to make a back-end-driven form, whereas all fields and their constraints are being sent to us by a server. Whilst I managed to get the constraints to be added/removed as we please by creating a simple directive, it seems like a form controller is not picking up these changes, and so the form is always $valid. Have a look at this jsfiddle, here is the directive:

myApp.directive('uiConstraints', [function(){

function applyConstraints(element, newVal, oldVal){
    //remove old constraints
    if(oldVal !== undefined && oldVal !== null){
        for (var i = 0; i < oldVal.length; i++) {
            element.removeAttr(oldVal[i].key);
        }
    }

    //apply new constraints
    if(newVal !== undefined && newVal !== null){
        for (var i = 0; i < newVal.length; i++) {
            var constraint = newVal[i];
            element.attr(constraint.key, constraint.value);
        }
    }
}

function link(scope, element, attrs){
    scope.$watch(attrs.uiConstraints, function(newVal, oldVal){
        applyConstraints(element, newVal, oldVal);
    });
}

return {
    restrict : 'A',
    link : link
};

}]);

The required behavior is so it works like on the official angularjs plunker. However, it seems the FormController is being created before the directive populates constraints on the input fields, and updating these constraints doesn't update the corresponding values in the FormController.

Does any1 know if I can force the FormController to pickup the changes to constraints made by the directive? And if so, how? I have no idea where to even start... Thanks.

-- EDIT --
I couldn't get plunker to work (show to others my latest changes) so here is jsfiddle of what I have: latest
To more in detail describe the issue:

  1. go to the jsfiddle described
  2. if you remove the initial value from the textbox, it will become red (invalid), however the controller won't pick that up and will still show:

myform.$valid = true
myform.myfield.$valid = true

-- EDIT --
The bounty description doesn't recognize Stack Overflow formatting (like 2 spaces for new line etc) so here it is in more readable form:

Since this is still unsolved and interesting question I decided to start a bounty.
The requirements are:
- works on ancient AngularJS(1.0.3) and newer (if it can't be done on 1.0.3 but someone did it on newer version of angular I will award bounty)
- initially a field has no constraints on it (is not required, max and min not set etc)
- at any time constraints for the field can change (it can become required, or a pattern for the value is set etc), as well as any existing constraints can be removed
- all constraints are stored in a controller in an object or array
- FormController picks up the changes, so that any $scope.FormName.$valid is being changed appropriately when constraints on any fields in that form change

A good starting point is my jsfiddle.
Thanks for your time and good luck!


Solution

  • Check out this PLUNK

    .directive('uiConstraints', ["$compile",
    function($compile) {
      function applyConstraints(element, newVal, oldVal) {
        //apply new constraints
        if (newVal !== undefined && newVal !== null) {
          for (var i = 0; i < newVal.length; i++) {
            var constraint = newVal[i];
            element.attr(constraint.key, constraint.value);
          }
        }
      }
    
      return {
        restrict: 'A',
        terminal: true,
        priority: 1000,
        require: "^form",
        link: function(scope, element, attrs, formController) {
          var templateElement;
          var previousTemplate;
          templateElement = element.clone(); //get the template element and store it 
          templateElement.removeAttr("ui-constraints");// remove the directive so that the next compile does not run this directive again
          previousTemplate = element;
    
          scope.$watch(attrs.uiConstraints, function(newVal, oldVal) {
            var clonedTemplate = templateElement.clone();
            applyConstraints(clonedTemplate, newVal);
            clonedTemplate.insertBefore(previousTemplate);
    
            var control = formController[previousTemplate.attr("name")];
            if (control){
               formController.$removeControl(control);
            }
            if (previousTemplate) {
              previousTemplate.remove();
            }
    
            $compile(clonedTemplate)(scope);
            previousTemplate = clonedTemplate;
          });
        }
      };
    }]);
    

    The idea here is to set terminal: true and priority: 1000 to let our directive be compiled first and skip all other directives on the same element in order to get the template element. In case you need to understand more, check out my answer: Add directives from directive in AngularJS.

    After getting the template element, I remove the ui-constraints directive to avoid this directive getting compiled again and again which would add a $watch to the digest cycle every time we toggle the constraints.

    Whenever the constraints change, I use this template element to build a new element containing all the constraints without the ui-constraints directive and compile it. Then I remove the previous element from the DOM and its controller from the Form Controller to avoid leaking and problems caused by previous element's controller existing in the Form Controller.