4

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!

Daniel Gruszczyk
  • 5,379
  • 8
  • 47
  • 86
  • I've spent a bit of time looking into this because it's quite an interesting problem. The basic problem is that the `` element needs to be re-compiled with its new attributes. Use the $compile service to do this. I've got a half-working plunk but it's not fully functional so I've not put it as an answer. http://plnkr.co/edit/WlSyZQ6hPGA9B4oWRG43?p=preview – Michael Bromley Aug 26 '14 at 15:15

4 Answers4

2

For starters, AngularJS has ng-required directive that allows you to toggle required validation

<input type="text" data-ng-required="isRequired" 
               data-ng-model="mydata" name="myfield" />

see the Fiddle:

Fiddle

Making Min and Max logic dynamic is a little more complex. Your current approach JQuery-like in alter elements which is NOT AngularJS strength. The ideal Answer is a directive like ng-minlnegth with more intelligence.

I'll look into that.

Dave Alperovich
  • 32,320
  • 8
  • 79
  • 101
2

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.

Community
  • 1
  • 1
Khanh TO
  • 48,509
  • 13
  • 99
  • 115
  • I find your answers to be very insightful and useful. Here, though, I have to ask: is this overkill? I don't like the idea of re-compiling templates or altering elements -- this feels too much like DOM manipulation. I prefer to create directives with behavior I want. This is not a criticism, but more of a Question. – Dave Alperovich Aug 30 '14 at 17:23
  • @Dave A: Because the constraints are dynamic, I cannot think of another solution than compiling the element dynamically. – Khanh TO Aug 31 '14 at 03:18
  • Thanks for your answer, as well as solving my problem I have learned a lot from it. I guess the bounty is going to you. thanks again :) – Daniel Gruszczyk Sep 01 '14 at 08:28
1

Quick and dirty solution:

scope.$watch(attrs.uiConstraints, function(newVal, oldVal){
    applyConstraints(element, newVal, oldVal);
    if (newVal != oldVal) {
        element = $compile(element)(scope);
    }            
});

I don't like solution and it's not perfect. This is an interesting problem, so I'm still working on finding a more elegant solution.

Yaron Schwimmer
  • 5,327
  • 5
  • 36
  • 59
  • This is pretty much what I came up with. However, I found that it will work one way (going from non-require to require), but when toggling back, it will stay in the require state. Can't figure out that one. Also, it does not alter the form.input.$valid etc. values. – Michael Bromley Aug 26 '14 at 15:19
  • As you both said it works partially. http://jsfiddle.net/eL3ce3oq/4/ I have to toggle 1st time in order for the $scope.myform.$valid to pick up, then it picks up ok both ways. $scope.myform.myfield.$valid doesn't pick up at all... – Daniel Gruszczyk Aug 26 '14 at 15:31
  • This would cause problems when we compile the element again and again because it compilesthe already compiled element. Every time a constraint is toggled, it would add a $watch into the digest,...This could cause the computer to hang if we toggle the constraints many times. – Khanh TO Aug 30 '14 at 13:16
0

Here is an example.

What went wrong for you was that you tried to use scope in your apply thing, and you were watching the wrong thing.

I changed your link function:

 link : function(scope, element, attrs){
            scope.$watch(attrs.constraints, function(newConstraints, oldConstraints){
              applyConstraints(newConstraints, oldConstraints, element);
            }, true);
        }

And your toggle function now does some random attributes and stuff:

$scope.toggleConstraints = function(myconstraints){
    var getRandomPropOrValueName = function(){
      return Math.random().toString(36).substring(7);
    }
    $scope.myconstraints = [{key: getRandomPropOrValueName(), value: getRandomPropOrValueName() }];
  };
Prashant Pokhriyal
  • 3,727
  • 4
  • 28
  • 40
Nikola Yovchev
  • 9,498
  • 4
  • 46
  • 72
  • as a matter of fact I realized that plunker was for some reason redirecting people to an older version of stuff. I couldn't figure out why so I moved over to jsfiddle. Sorry for wasting your time on older, solved issue. I updated my original post with jsfiddle. – Daniel Gruszczyk Aug 26 '14 at 15:00