Reputation: 428
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
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