4

I have a custom dropdown directive that has common attributes such as class and ng-model.

I have decided to extend this control for support for validation and now need to include optional attributes that should only get included in the output template if they are set by the programmer.

Sample

Sample Directive Call

I have a partially working system in which I moved my code out of a template URL and into a string concatenation which I call in the post: function of the directives compile.

I would have preferred to leave my directives HTML in a template, but could not get that working so I have this solution.

Questions:

  1. Is this the best way to write a template with dynamic attributes?
  2. Could this be made to work while leaving the HTML in a template URL
  3. Should I be using the compile => post function or should this be done in the link function

Code for Directive

'use strict';

angular.module(APP)
  .directive('wkKeyLabelSelect', ["$compile",
    function($compile) {
      return {
        restrict: 'EA',
        replace: true,
        scope: {
          'class': '@',              // Permanent - One Way Attribute
          ngModel: '=',              // Permanent - Two Way Attribute (Angular)
          items: '=',                // Permanent - Two Way Attribute (Custom)
          id: '@',                   // Dynamic - One Way Attribute
          name: '@',                 // Dynamic - One Way Attribute
          ngRequired: '=',           // Dynamic - Two Way Attribute (Angular) 
      },
        //templateUrl: COMPONENTS_PATH + '/keyLabelSelect/keyLabelSelect.html',
        controller: 'KeyLabelSelectController',
        link: function (scope, element, attrs) {
          //$compile(element)(scope);
        },
        compile: function (element, attrs) {

          // name & ngRequired are not available in the compile scope
          //element.replaceWith($compile(html)(scope));

          return {
            pre: function preLink(scope, iElement, iAttrs, controller) {

            },
            post: function postLink(scope, iElement, iAttrs, controller) {

              // Template goes here
              var html =
                '<select ' +
                  ' class="{{class}}"' +
                  (scope.id ? ' id="{{id}}"' : "") +
                  (scope.name ? ' name="{{name}}"' : "") +
                  (scope.ngRequired ? ' ng-required="true"' : "") +
                  ' ng-model="ngModel"' +
                  ' ng-options="item.key as item.label for item in items"' +
                  '>' +
                '</select>';

              iElement.replaceWith($compile(html)(scope));
            }
          }
        }
      };
    }
  ]);

Code for Directive Controller

angular.module(APP)

.controller('KeyLabelSelectController', ['$scope', function ($scope) {

  $scope.klass = typeof $scope.klass === 'undefined' ? 'form-control' : $scope.klass;

  console.log($scope.ngModel);
  console.log($scope.items);

}]);

HTML used to run the directive

<div class="form-group" ng-class="{ 'has-error': editForm.state.$touched && editForm.name.$invalid }">
    <label class="col-md-3 control-label">State</label>
    <div class="col-md-9">
        <wk-key-label-select id="state" name="state"
                                ng-required="true"
                                ng-model="model.entity.state"
                                class="form-control input-sm"
                                items="model.lookups.job_state">
        </wk-key-label-select>

        <div class="help-block" ng-messages="editForm.state.$error">
            <p ng-message="required">Job State is required.</p>
        </div>
    </div>

</div>

My Original Template URL content, not used currently

<!-- This is now deprecated in place of inline string -->
<!-- How could I use a in place of string concatenation  -->

<select class="{{klass}}"
        name="{{name}}"
        ng-model="ngModel"
        ng-options="item.key as item.label for item in items"></select>

Questions

5
  • 1
    Why do you have code screenshots that are exactly the same as the code you pasted? Also, you seem to be implementing a custom input control, but then you should be using ngModelController properly - doing scope: {ngModel: "="} is the wrong way to go. Commented Sep 9, 2015 at 2:51
  • The images are annotated with the specific questions, pointing to the code in question. Thats why it looks like dupicate, I have both the code and the annotated image there Commented Sep 9, 2015 at 3:05
  • I was not aware of ngModelController, doing my research on that now Commented Sep 9, 2015 at 3:06
  • in response to "Should be using ngModelController peroperly", I've done some research because I'm not doing custom validation or complex properties, there is no need to introduce the complexity of using ngModelController. Source for my opinion is: radify.io/blog/… and bennadel.com/blog/… Commented Sep 9, 2015 at 3:18
  • You are introducing a custom input control. Sure, it uses an existing input control in its template, but if you want it to support the ngModel model (i.e. support other validators, custom parsers, integrate with <form>, etc), then it's a good idea to use ngModelController. You don't have to, but that's the most Angular-way-ish. Overall, you're asking an XY question - instead, explain what you are trying to achieve with your custom dropdown that normal <select> cannot provide Commented Sep 9, 2015 at 3:29

1 Answer 1

2

The "proper" way to introduce a custom input controller is to support the ngModelController. This enables your custom control to integrate with other directives that support ngModel, like custom validators, parsers, <form>s. This is a bit tricky, but makes your control indistinguishable from built-in controls for the framework:

.directive("customSelect", function() {
  return {
    require: "?ngModel",
    scope: {
      itemsExp: "&items" // avoids the extra $watcher of "="
    },
    template: '<select ng-model="inner" \
                       ng-options="item.key as item.label for item in itemsExp()"\
                       ng-change="onChange()"></select>',
    link: function(scope, element, attrs, ngModel) {
      if (!ngModel) return;

      // invoked when model changes
      ngModel.$render = function() {
        scope.inner = ngModel.$modelValue;
      };

      scope.onChange = function() {
        ngModel.$setViewValue(scope.inner);
      };
    }
  };
});

Then, it can neatly integrate with other controls and leverage validators likes ng-required natively:

<custom-select name="c1" ng-model="c1" items="items" ng-required="true">
</custom-select>

Demo

It may not seem like the answer to the question you asked, but that is only because your question is a bit of an XY question. By implementing a custom input control, you achieve what you set out to do - assign name attribute to a directive (which registers itself with the form directive, if it is provided) and ng-required works natively. However, if you must assign name/id to the underlying <select> (for CSS reasons or whatnot), you could use ng-attr- to conditionally apply an attribute. The template would change to:

<select ng-attr-name="attrs.name || undefined"
        ng-attr-id  ="attrs.id   || undefined"
        ng-model="inner" ...

Of course, you'd need to expose attrs on the scope in the link function with:

link: function(scope, element, attrs, ngModel){
  scope.attrs = attrs;

  // etc...
}
Sign up to request clarification or add additional context in comments.

3 Comments

That 2nd second on ng-attr was a real help, I read up on it here: thinkingmedia.ca/2015/03/…
Mostly, I have had trouble with the optional attributes (<select ng-attr-name="attrs.name || undefined"), but the other stuff around using ngModel controller has been a real help
@DavidCruwys, what's the trouble with the optional attributes?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.