Fooberichu
Fooberichu

Reputation: 428

AngularJS - Directive/Scope Issue

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(); });
            }
        }
    });

Upvotes: 1

Views: 356

Answers (1)

Chad Robinson
Chad Robinson

Reputation: 4623

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...

Upvotes: 2

Related Questions