0

Please check this code sample here:

http://plnkr.co/edit/zQ049VsfrprutNYkZm3y?p=preview

This code sample uses the directive check-if-required to add ng-required attribute. When page loading is completed, the input element with test_me is set as required.

Now suppose at some point later, the list of required fields has changed, how is it possible to re-run that directive in order to change the ng-required attribute of the new fields that have become required.

Tarek.


UPDATE 2:

After a lot of research and trials, I realized that this request is not practical and should not be approached. The problem I was trying to solve is slow loading of HTML in IE11 due to adding validation rules during runtime.

See related post here in attempt to solve the problem.

Also, check the final work here.

I was trying to modify the HTML attributes/directives to add validation such as ng-reuqired and then use $compile as this is required by AngularJS to have dynamic validation.

After reading the solutions and comments added here, I think there is a better option.

Instead of modifying the element HTML by adding 'ng-required' directive, then compile, instead, I can skip HTML and use the ngModel.NgModelController of the related HTML Element, then access the $validators to perform validation using code. If you read the code here, you will see that I have already accessed the ngModel.NgModelController for each element in variable elmModel. I think this variable will provide access to $validators which can be used to add validation to the element. Since the rules are now available in validationList variable, I will write a function to perform validation by looking up this list and apply the available validation on-the-fly.

This will be the improvement in the future sprints.

If you need such solution you may keep and eye on this post as I am planning to implement the newly proposed approach.

This post can be closed for now.

Tarek


UPDATE1:

As I am replying to the comments, the solution flashed in my head... so thank you all for forcing me to think to get the solution.

Simply, I will create a scope function isFieldRequired() inside the directive check-if-required. This function will accept the element ID and if the element ID is found it the list of required fields listRequiredFields, it will return true, and false otherwise. At this point in time, I have access to the element ID so there is no problem here. Eventually, the HTML will look something like the following:

<input id="some_id" type="text" ng-required="isFieldRequired('some_id')">

Finally, the $compile service will be executed over the modified HTML element.

Below is the high-level structure of the directive check-if-required:

app.directive('checkIfRequired', ['$compile', '$timeout', '$interpolate', function ($compile, $timeout, $interpolate) {
    return {
        //JIRA: NE-2682 - trying to solve issue of interference between datepicker popup box and `$compile` service.
        priority: 100,
        terminal: true,
        require: '?^form',
        link: function (scope, el, attrs, ngForm) {
            scope.isFieldRequired = function (prmFieldName) {
                var isFound;
                isFound = scope.listRequiredFields.indexOf(prmFieldName) !== -1;
                return isFound;
            }
            var children;
            //Use timeout to give chance for inner block elements to finish rendering
            $timeout(function() {
                children = $(":input", el);
                angular.forEach(children, function(child, key) {
                    var childID = (child?child.id:"") || "";
                    if (childID.indexOf("{{") >= 0) {
                        childID = $interpolate(childID)(scope);
                    }
                    if(child && childID) {
                        angular.element(child).attr("ng-required", "isFieldRequired('" + childID + "')");
                    }
                });
                $compile(el, null, 100)(scope);
            }, 100)
        }
    };
}]);

To use check-if-required add it to any parent element as follows:

<div ng-form="masterForm" check-if-required>
    <label>First Name:</label><input id="firsName" type="text"><br>
    <label>Last Name:</label><input id="lastName" type="text"><br>
    <label>Age:</label><input id="age" type="number"><br>
    <label>Address:</label><input id="address" type="text"><br>
</div>

When I implement the above successfully, I will add it as a solution.

The only drawback of the above solution is that the ng-required directive will be added for all fields even if not relevant. That is why I was searching for a way to re-apply the directive check-if-required.

Tarek

tarekahf
  • 738
  • 1
  • 16
  • 42
  • I guess I am slightly confused. Are you saying during the use of the app because they do something or other a field is now required? If so I guess I would write a custom directive for this specific need. Sorry If I am confused. Just trying to understand the bits and pieces mate. – Maccurt Jun 09 '17 at 16:41
  • Why do you need this extra complexity? `ng-required` is not a simple attribute, [it is a directive](https://docs.angularjs.org/api/ng/directive/ngRequired) and it simply adds required attribute to your field, so you can add it to your element initially and pass the variable from the `$scope` to toggle the required attribute. – Stanislav Kvitash Jun 09 '17 at 17:14
  • That is because the list of required fields is placed in an array using the element ID. I had to use the Element ID since it is unique. The list is stored in DB and loaded during page load. I wanted to avoid adding `ng-required` directive manually for each and every field since we have more than 500 fields, not all of them are required!! I hope this clarifies your query. – tarekahf Jun 09 '17 at 18:36

2 Answers2

1

First of all, you are manipulating the DOM with JQuery for no reason as far as I can tell. There are sometimes reasons to use JQuery in Angular, but more often it is a sign that you are not using Angular correctly.

Regarding the problem at hand, it's not entirely clear why you are using ids in the first place. ng-required is a directive that can take a scope variable as it's argument, and will update when that scope variable changes. To change whether things are required, simply keep several variables in scope and change them based on your needs.

I modified your Plunkr to add a scope variable that controls whether the fields are required.

(function(angular) {
  'use strict';
var app=angular.module('ui.bootstrap.demo', ['ngAnimate', 'ui.bootstrap']);
app.controller('DatepickerPopupDemoCtrl', ['$scope', function ($scope) {
  $scope.today = function() {
    $scope.dt = new Date();
    $scope.dt2 = new Date();
  };
  
  // I recommend bundling variables that just control ui to keep your scope tidy
  $scope.ui = {
    datepicker: {
      required: true
    },
    datepicker2: {
      required: false
    },
    format: {
      required: true
    }
  }
  
  $scope.today();
  $scope.clear = function() {
    $scope.dt = null;
    $scope.dt2 = null;
  };

  $scope.inlineOptions = {
    customClass: getDayClass,
    minDate: new Date(),
    showWeeks: true
  };

  $scope.dateOptions = {
    dateDisabled: disabled,
    formatYear: 'yy',
    maxDate: new Date(2020, 5, 22),
    minDate: new Date(),
    startingDay: 1
  };

  // Disable weekend selection
  function disabled(data) {
    var date = data.date,
      mode = data.mode;
    return mode === 'day' && (date.getDay() === 0 || date.getDay() === 6);
  }

  $scope.toggleMin = function() {
    $scope.inlineOptions.minDate = $scope.inlineOptions.minDate ? null : new Date();
    $scope.dateOptions.minDate = $scope.inlineOptions.minDate;
  };

  $scope.toggleMin();

  $scope.open1 = function() {
    $scope.popup1.opened = true;
  };

  $scope.open2 = function() {
    $scope.popup2.opened = true;
  };

  $scope.setDate = function(year, month, day) {
    $scope.dt = new Date(year, month, day);
    $scope.dt2 = new Date(year, month, day);
  };

  $scope.formats = ['dd-MMMM-yyyy', 'yyyy/MM/dd', 'dd.MM.yyyy', 'shortDate'];
  $scope.format = $scope.formats[0];
  $scope.altInputFormats = ['M!/d!/yyyy'];

  $scope.popup1 = {
    opened: false
  };

  $scope.popup2 = {
    opened: false
  };

  var tomorrow = new Date();
  tomorrow.setDate(tomorrow.getDate() + 1);
  var afterTomorrow = new Date();
  afterTomorrow.setDate(tomorrow.getDate() + 1);
  $scope.events = [
    {
      date: tomorrow,
      status: 'full'
    },
    {
      date: afterTomorrow,
      status: 'partially'
    }
  ];

  function getDayClass(data) {
    var date = data.date,
      mode = data.mode;
    if (mode === 'day') {
      var dayToCheck = new Date(date).setHours(0,0,0,0);

      for (var i = 0; i < $scope.events.length; i++) {
        var currentDay = new Date($scope.events[i].date).setHours(0,0,0,0);

        if (dayToCheck === currentDay) {
          return $scope.events[i].status;
        }
      }
    }

    return '';
  }
}])

})(window.angular);
<!DOCTYPE html>
<html ng-app="ui.bootstrap.demo">

  <head>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular.js"></script>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular-animate.js"></script>
    <script src="//angular-ui.github.io/bootstrap/ui-bootstrap-tpls-1.3.3.js"></script>
    <script src="example.js"></script>
    <link href="//netdna.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet" />
  </head>

  <body ng-controller="DatepickerPopupDemoCtrl">
    <style>
  .full button span {
    background-color: limegreen;
    border-radius: 32px;
    color: black;
  }
  .partially button span {
    background-color: orange;
    border-radius: 32px;
    color: black;
  }
  input.ng-invalid-required {
      background-color:yellow;
  }
    </style>
    <fieldset check-if-required>
      <div >
        <pre>Selected date is:           <em>{{dt | date:'fullDate' }}</em>
        </pre>
        <h4>Popup</h4>
        <div class="row">
          <div class="col-md-6">
            <p class="input-group">
              <input
                type="text"
                class="form-control"
                uib-datepicker-popup="{{format}}"
                ng-model="dt" is-open="popup1.opened"
                datepicker-options="dateOptions"
                close-text="Close"
                alt-input-formats="altInputFormats"
                ng-required="ui.datepicker.required"
              />
              <span class="input-group-btn">
                <button type="button" class="btn btn-default" ng-click="open1()">
                  <i class="glyphicon glyphicon-calendar"></i>
                </button>
              </span>
            </p>
          </div>
          <div class="col-md-6">
            <h4>Using $compile with Datepicker Popup</h4>
            <p class="input-group">
              <input
                id="test_me"
                type="text"
                class="form-control"
                uib-datepicker-popup=""
                ng-model="dt2"
                is-open="popup2.opened"
                datepicker-options="dateOptions"
                close-text="Close"
                ng-required="ui.datepicker2.required"
              />
              <span class="input-group-btn">
                <button type="button" class="btn btn-default" ng-click="open2()">
                  <i class="glyphicon glyphicon-calendar"></i>
                </button>
              </span>
            </p>
          </div>
        </div>
        <div class="row">
          <div class="col-md-6">
            <label>Format:<span class="muted-text">(manual alternate<em>{{altInputFormats[0]}}</em>
)</span>
            </label>
            <select
              class="form-control"
              ng-model="format"
              ng-options="f for f in formats"
              ng-required="ui.format.required"
            >
              <option></option>
            </select>
          </div>
        </div>
        <div class="row">
          <div class="col-md-6">
            <label>Test ng-init after using terminal = true or flase:
            </label>
            <input class="form-control" ng-model="testTerminal" ng-init="testTerminal = 'It is working'">
            <span>Test terminal=true using interpolation: {{testTerminal}} </span><br>
            <span>Comment out the line 'terminal: true' to check the result</span>
          </div>
        </div>
        <hr />
        <button type="button" class="btn btn-sm btn-info" ng-click="today()">Today</button>
        <button type="button" class="btn btn-sm btn-default" ng-click="setDate(2009, 7, 24)">2009-08-24</button>
        <button type="button" class="btn btn-sm btn-danger" ng-click="clear()">Clear</button>
        <button type="button" class="btn btn-sm btn-default" ng-click="toggleMin()" uib-tooltip="After today restriction">Min date</button>
      </div>
    </fieldset>
  </body>

</html>
VivaLaPanda
  • 809
  • 7
  • 24
  • Good job. That is exactly what I meant when was writing my comment! This plnkr was too long for me to write my own answer :) – Stanislav Kvitash Jun 09 '17 at 17:24
  • Yes, I fully understood your approach. To clarify the situation, the list of required fields is placed in a scope variable using the element 'ID' for example: `$scope.listRequired = ['designation', 'first_name', 'last_name']`. Now based on that how would you add a scope function that would decide if the related field is a member of the list `listRequired`? I didn't find a way to access the element ID from a scope function. Mind you the element ID sometimes is based in interpolated expression as such ` ng-required='checkRequired(????)'`. – tarekahf Jun 09 '17 at 18:28
  • So to clarify, you get back a list of IDs from some external service, and know those IDs are required? Because if the list is local why not just manually set the required tags? You could also use $watch in the `id={{scopeExpr}}` case. In the worst case name your required object like: `$scope.ui.[elemId]`. Then when you get the required list iterate through it and set `$scope.ui[elem].required = true;`. – VivaLaPanda Jun 09 '17 at 18:37
  • @AdrianSmith: Yes, that would work. However, I want to eliminate the need to manually add `ng-required` directive for each and every field. I have updated the description to clarify. I think I found a solution. – tarekahf Jun 09 '17 at 20:17
  • Okay, you should add your solution as an answer to the question and mark it as solved. Also, if you have over 500 fields, you are using ng-repeat right? You can just add ng-required on the repeated element. – VivaLaPanda Jun 09 '17 at 20:32
0

Make the required input field as required and recompile the directive using $compile. Here is working plunker

In the plunker I added last button to make input field with id make_require to be required. And when someone click on that button ng-required attribute will be added with value true to that input

  <button type="button" class="btn btn-sm btn-info" ng-click="today()">Today</button>
    <button type="button" class="btn btn-sm btn-default" ng-click="setDate(2009, 7, 24)">2009-08-24</button>
    <button type="button" class="btn btn-sm btn-danger" ng-click="clear()">Clear</button>
    <button type="button" class="btn btn-sm btn-default" ng-click="toggleMin()" uib-tooltip="After today restriction">Min date</button>
    <button type="button" class="btn btn-sm btn-primary" ng-click="makeRequired()">Make input Required</button>

Added this js code in the controller

  $scope.makeRequired = function() {
    $("#make_require").attr('ng-required', 'true');
    $compile(angular.element($("fieldset")))($scope)  //compile the directive again
  }
Mr_Perfect
  • 8,254
  • 11
  • 35
  • 62
  • Thanks! But how to access the element ID dynamically? And how to avoid the need for adding `ng-click` for each and every field to make it required if needed? Could you please base your solution on the existing code under `check-if-required` directive? – tarekahf Jun 09 '17 at 18:38
  • In your question, you said, **at some point later, the list of required fields has changed**. Be more specific to do answer. At which point required fields may change and what are those? I just take button click as **at somepoint** and first input field as **input field that become required in the future** – Mr_Perfect Jun 09 '17 at 18:42
  • Imagine when you click on the button you added, you add an element 'ID' to an array in a scope variable, instead of adding the ng-required attribute directly on the HTML. Then, this must be reflected dynamically. Check my update in the description above. – tarekahf Jun 09 '17 at 18:51
  • As your description, you are right.Do that but make sure to recompile the element using `$compile` after adding required fields in the array – Mr_Perfect Jun 09 '17 at 18:57