Sign up ×
Stack Overflow is a community of 4.7 million programmers, just like you, helping each other. Join them; it only takes a minute:

I have an old jQuery plugin I wrote that I'd like to carry over similar functionality to an angular directive now. Long story short I want it to transform a select tag into the following HTML.

<div class="a">
  <span class="b" />
  <span class="c">{{text}}</span>
  <select></select> <!-- this is the original select -->
</div>

The select would look similar to this:

<select data-ng-options="s.SomeValue as s.SomeLabel for s in someScopeArray" 
  data-ng-change="notifyForSomeFun()" data-ng-model="someValue"></select>

I have tried to use a "link" function on the new directive and simply do the same sort of jQuery wrapping and appending to add the HTML I wanted around the <select> like I would have done in my normal jQuery plugin, but where I start to break down is needing to update the ng-model applied to the <select> and also update the {{text}} binding on the <span class="c">.

Here is my latest bastardized attempt:

var selectWrapperDirective = function () {
        return {
            restrict: 'A',
            scope: true,
            link: function (scope, element, attrs) {
                var $this = element;

                var defaults = {
                    containerClass: 'selectWrapper',
                    labelClass: 'label',
                    dropImageClass: 'dropImage'
                };

                var itemText = function () {
                    return $this.find('option:selected').text();
                };

                var opts = angular.extend({}, defaults, scope.$eval('{' + attrs.selectWrapper + '}'));

                var oldNgChange = attrs['ngChange'];

                var ngModel = attrs['ngModel'];
                if (ngModel) {
                    console.log('setting new ngmodel');
                    attrs.$set('ngModel', '$parent.' + ngModel);
                }

                // wrap it in a div and add the class, add a span with label class, add a span with dropimage class
                $this.wrap('<div class="' + opts.containerClass + '" />');
                var container = $this.parent();
                container.prepend('<span class="' + opts.dropImageClass + '" />');
                container.prepend('<span class="' + opts.labelClass + '">' + itemText() + '</span>');

                attrs.$set('ngChange', 'onItemChanged()');

                scope.onItemChanged = function () {
                    console.log('onItemChanged fired');
                    $this.closest('div').find('.' + opts.labelClass).text(itemText());

                    if (oldNgChange)
                        scope.$parent.$eval(oldNgChange);
                };
            }
        }
    };

I'll be the first to admit that when it comes to directives my head is exploding. I know that I'm having some sort of issue with scope. This current version will update the {{text}} when it fires the onItemChanged but it isn't updating the model in the parent. I need to use this directive a bunch of times with a bunch of different models throughout the page so that is where I was thinking I needed some sort of isolated scope. HALP!

EDIT I've accepted Chad Robinson's answer and noted a comment there. Ultimately his answer, paired with the SO question/answer at AngularJS: Dropdown directive with custom ng-options was what led me to my solution.

app.directive('selectWrapper', function () {
        return {
            replace: true,
            restrict: 'E',
            scope: {
                items: '=',
                ngModel: '='
            },
            template: function () {
                return '<div ng-class="config.containerClass">' +
                    '<span ng-class="config.labelClass">{{labelText}}</span>' +
                    '<span ng-class="config.dropImageClass"></span>' +
                    '<select ng-model="ngModel" ng-options="a[optValue] as a[optDescription] for a in items" ng-change="valChanged()"></select>' +
                    '</div>';
            },
            link: function (scope, element, attrs) {
                var defaults = {
                    containerClass: 'selectWrapper',
                    labelClass: 'label',
                    dropImageClass: 'dropImage'
                };

                scope.labelText = '';

                var config = angular.isDefined(attrs.config) ? attrs.config : '';
                console.log(config);
                scope.config = angular.extend({}, defaults, scope.$eval('{' + config + '}'));

                scope.optValue = attrs.optValue;
                scope.optDescription = attrs.optDescription;

                var setVal = function () {
                    scope.labelText = element.find('option:selected').text();
                };

                var oldNgChanged = attrs.ngChange;
                scope.valChanged = function () {
                    setVal();

                    if (oldNgChanged)
                        scope.$parent.$eval(oldNgChanged);
                };

                // initial label text via watcher
                var unbindwatcher = scope.$watch(scope.ngModel, function () { setVal(); unbindwatcher(); });
            }
        }
    });
share|improve this question
    
since you are creating a new scope in your directive, you might have to use data-ng-model="$parent.someValue" – Arun P Johny Aug 21 '14 at 3:42
    
also can you edit [this fiddle ](jsfiddle.net/arunpjohny/rfa3aL8t/1) to recreate the problem – Arun P Johny Aug 21 '14 at 3:45
    
I updated the fiddle jsfiddle.net/rfa3aL8t/3 so it works right now exactly the way I have it. I started stubbing in an element-based directive per Chad's answer, but didn't get too far on that one yet in the fiddle. One issue you'll see I have, as well, is if the model gets updated somewhere else outside of there I need to be able to react and change the label accordingly. – Fooberichu Aug 21 '14 at 14:42

1 Answer 1

up vote 2 down vote accepted

It's possible to finish what you started, but you're jumping through a lot of hoops that AngularJS has a way around. Consider using something like <my-select> instead of <select>. Then you could do something like:

var selectWrapperDirective = function () {
    return {
        restrict: 'E',
        replace: true,
        template: '<div class="a"><span class="b" />' +
                  '<span class="c">{{text}}</span>' +
                  '<select></select></div>',
        link: function ($scope, iElement, iattrs) {
            // ...
        }
    };
};

When Angular compiles your directive it's going to interpolate everything in the template and then run your linking function. You can still use any/all attributes you originally wanted applied to the <select> element from inside your template, and you can still register change listeners on things, although you would typically do something like <select ng-change="selectionChanged();"></select> and define $scope.selectionChanged = function() { ... }; in your linking function - it saves a lot of work over manually binding on element change events directly.

IMO element directives are one of the three or four biggest advantages of AngularJS - they give you everything the Polymer/WebComponents folks are working toward, today, and in a very powerful way. It'd be a shame not to use them here because your use-case just screams their name...

share|improve this answer
    
Chad, this sounds like a direction that would probably work better if I could wrap my head around it. I'm a little unsure, however, how I would bind the ng-options to the select from higher up. Assuming I was taking a direct translation from a regular "select", the "my-select" would be something like <my-select data-ng-options="s.SomeValue as s.SomeLabel for s in someArray" data-ng-model="someLocalValue" data-ng-change="localNotify()"></my-select>. I already have everything on the form working with a normal select, just need to get this wrapper on it now. – Fooberichu Aug 21 '14 at 14:11
    
I've modified the jsFiddle from Arun's comments under my question. Here is a fiddle edit where I'm attempting the element-based directive: jsfiddle.net/rfa3aL8t/4 – Fooberichu Aug 21 '14 at 15:09
    
I finally got back to this after having some other work come up that took precedence. Using Chad's answer, paired with this one: stackoverflow.com/questions/14967416/… allowed me to figure the problem out. I'm going to mark Chad's answer as the accepted answer and then add an addendum somewhere with my finished result. – Fooberichu Sep 11 '14 at 21:48

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.