Angular dynamic polymorphic directives - angularjs

I'm a beginner to Angular but am poking into some slightly more advanced corners to ensure it has the features I need.
Specifically I need to:
render a sequence of widgets of different types with each implemented as an independent Angular directive
the widget type is determined from the data, not by markup
widgets are each defined in a separate file
set the scope of the directive to the data for that widget instance
I think I have solved the requirement described below and implemented at http://jsfiddle.net/cUTt4/5/
Questions:
Is this correct, best practice, and reasonably fast?
Any improvements I should add?
It would be better if the widget directives had no explicit reference { item : '=' } to obtain their isolated scope, but their sub-scopes should be built by the renderform directive. How do I do that?
My solution:
HTML
(Note the Angular templates are in script here due to limitations of jsfiddle)
<div ng-app="myApp">
<script type="text/ng-template" id="widget-type-a">
<div>
<label>{{ item.label}} </label>
<input type="text" ng-model="item.val" >
</div>
</script>
<script type="text/ng-template" id="widget-type-b">
<div>
<label>{{ item.label}}</label>
<input type="text" ng-model="item.val" >
</div>
</script>
<div ng-controller="FormCtrl">
<renderform></renderform>
</div>
</div>
main.js :
var app = angular.module('myApp', []);
function FormCtrl($scope) {
items = [
{
type: 'widget-type-a',
label : 'Widget A instance 1',
val: 1
},
{
type: 'widget-type-b',
label : 'Widget B instance 1',
val : 2
},
{
type: 'widget-type-a',
label : 'Widget A instance 2',
val : 3
}
];
$scope.items = items
}
app.directive('renderform', function($compile) {
function linkFn(scope, element) {
var item,
itemIdx,
templStr = '',
newParent,
data,
newEl;
newParent = angular.element('<div></div>')
for(itemIdx in scope.items) {
item = items[itemIdx];
templStr += '<div ' + item.type + ' item="items[' + itemIdx + ']"></div>';
}
newEl = angular.element(templStr);
$compile(newEl)(scope);
element.replaceWith(newEl);
}
return {
restrict: 'E',
link:linkFn
};
});
app.directive('widgetTypeA', function() {
return {
restrict: 'A',
templateUrl: 'widget-type-a',
scope: { item: '=' }
};
});
app.directive('widgetTypeB', function() {
return {
restrict: 'A',
templateUrl: 'widget-type-b',
scope: { item: '='}
};
});

sorry fast answer, not tested :
<div data-ng-repeat="item in items">
<div data-ng-switch data-on="item.type">
<div data-ng-switch-when="widget-type-a" data-widget-type-a="item"></div>
<div data-ng-switch-when="widget-type-b" data-widget-type-b="item"></div>
</div>
</div>
If this is what you're looking for, please improve this answer.

I have been thinking about this problem for some time now and, although the ng-switch option work for simple cases, it introduces quite a maintenance overhead.
I've come up with a solution which allows for a single point of maintenance. Consider the following:
var app = angular.module('poly', []);
app.controller('AppController', function ($scope) {
$scope.items = [
{directive: 'odd-numbers'},
{directive: 'even-numbers'},
{directive: 'odd-numbers'}
];
});
app.directive('component', function ($compile) {
return {
restrict: 'E',
controller: function () {
},
controllerAs: 'component_ctrl',
link: function (scope, element, attrs) {
var child_directive = scope.$eval(attrs.directive);
var child_element = $compile('<' + child_directive + ' data="data"></' + child_directive + '>')(scope);
element.append(child_element);
}
}
});
app.directive('oddNumbers', function ($interval) {
return {
restrict: 'E',
link: function (scope) {
scope.number = 0;
$interval(function () {
scope.number += 2;
}, 1000);
},
template: '<h1>{{ number }}</h1>'
}
});
app.directive('evenNumbers', function ($interval) {
return {
restrict: 'E',
link: function (scope) {
scope.number = 1;
$interval(function () {
scope.number += 2;
}, 1000);
},
template: '<h1>{{ number }}</h1>'
};
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<section ng-app="poly" ng-controller="AppController">
<div ng-repeat="item in items">
<component directive="item.directive" data="item.data"></component>
</div>
</section>
This allows for the components to be specified in the controller in an ad hoc way and the repeater not having to delegate responsibility via a switch.
NB I didn't implement how the data is passed between components

Related

AngularJs, when directive compile new attributes, the events are not triggered if there is a scope

I'm a bit stuck on an directive which add attributes and recompile the element.
If I had a scope on the directive ng-change is not triggered anymore (without it it works). I based my test on this answer
The HTML
<div ng-app="myApp">
<div ng-controller='testController'>
<div ng-repeat="field in fields">
<input type="text" ng-model="ngModel[field.fieldName]" property="{{formParams.getProperties(field.fieldName)}}" update-attr ng-change="test()" />
</div>
</div>
</div>
The directive:
angular.module('myApp', [])
.controller('testController', function ($scope) {
$scope.properties = {
"something": {
"style": "float:left;"
},
"something2": {
"style": "float:right;"
}
};
$scope.ngModel = {};
$scope.fields = [{
fieldName: 'something'
}, {
fieldName: 'something2'
}];
$scope.test = function () {
alert('i dont get triggered');
};
$scope.formParams = {
getProperties: function (fieldName) {
return $scope.properties[fieldName];
}
};
})
.directive('updateAttr', function ($compile) {
return {
restrict: 'A',
replace: true,
terminate: true,
scope: {
ngModel : '='
},
link: function (scope, elem, attrs) {
if (angular.isDefined(attrs['property']) && attrs['property'].lenght != 0) {
var json = JSON.parse(attrs['property']);
angular.forEach(json, function (value, key) {
elem.attr(key, value);
});
elem.removeAttr('property');
var $e = $compile(elem[0].outerHTML)(scope);
elem.replaceWith($e);
}
}
};
});
Here a fork of the fiddle to test with a scope on the directive: fiddle
Do you have any suggestion ?
I found why ng-change was not trigger so I share the answer:
When we add scope attribute on the directive, a new scope is created. So we have to use $scope.$parent for the compilation. I have updated the fiddle with the correction.

AngularJS: How to implement a directive that outputs its markup?

DEMO
Imagine I have some markup, e.g.:
<my-input model="data.firstName"></my-input>
Now, I would like to create a my-markup directive that will add a button to show/hide its markup.
So, this:
<div my-markup>
<my-input model="data.firstName"></my-input>
</div>
should result in this:
and when the button is clicked, the markup should appear:
The my-markup directive should not break any data bindings of its children.
Here is my attempt to implement this.
The markup appears, but the button doesn't work. Any ideas how to fix this?
PLAYGROUND HERE
Here is my approach. Couple of things:-
1) Instead of isolated scope on myMarkup, create a child scope, ultimately the actual directive myInput will be isolated. This would be required if you do need to support multiple myMarkup directive under the same scope.
2) You need a click event on the button, i wouldn't do logic on the markup instead abstract out to a method on the scope.
3) You would just need one button, do not need 2 buttons. Just change the text of the button.
.directive('myMarkup', function($compile) {
return {
restrict: 'A',
scope: true, //Create a child scope
compile: function(element) {
//Just need one button
var showButton = '<button ng-click="toggleMarkup()">{{model.showMarkup ? "Hide": "Show"}} Markup</button>';
var markup = '<pre ng-show="model.showMarkup">' + escapeHtml(element.html()) + '</pre>';
//append the markups
element.append(showButton).append(markup);
return linker;
}
};
function linker(scope, element) {
scope.model = {
showMarkup: false
};
//Click event handler on the button to toggle markup
scope.toggleMarkup = function(){
scope.model.showMarkup = !scope.model.showMarkup;
}
};
});
Demo
Please see below
function escapeHtml(html) {
return html.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
angular.module('App', []).controller('AppCtrl', function($scope) {
$scope.data = {
firstName: 'David'
};
}).directive('myInput', function() {
return {
restrict: 'E',
scope: {
model: '='
},
template: '<input class="my-input" type="text" ng-model="model">'
};
}).directive('myMarkup', function() {
return {
restrict: 'A',
scope: {},
link: function(scope, elem, attr) {
},
compile: function(element) {
var showButton = '<button ng-if="data.showMarkup" ng-click="data.showMarkup=!data.showMarkup">Hide Markup</button>';
var hideButton = '<button ng-if="!data.showMarkup" ng-click="data.showMarkup=!data.showMarkup">Show Markup</button>';
var markup = '<pre ng-if="data.showMarkup">' + escapeHtml(element.html()) + '</pre>';
element.append(showButton);
element.append(hideButton);
element.append(markup);
return function(scope, element) {
scope.data = {
showMarkup: true
};
};
}
};
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<body ng-app="App" ng-controller="AppCtrl">
<pre>data = {{ data | json }}</pre>
<div my-markup>
<my-input model="data.firstName"></my-input>
</div>
</body>

Can directive isolated scope be used inside directive for calculations

If there is a directive defined can the scope passed to it through attributes, defined on it, be used inside this directive to get needed results for usage in template? i.e. I have such directive
var carAuction = angular.module('carAuction');
carAuction
.controller('mainCtrl', function($scope)
{
var car = {
comments: []
};
car.comments.push({
comment: 'Ok car',
rating: 1
});
car.comments.push({
comment: 'Nice car.',
rating: 2
});
car.comments.push({
comment: 'Awesome car!',
rating: 3
});
$scope.car = car;
})
.directive('carCommentRaiting', function()
{
return
{
restrict: 'E',
templateUrl: 'path/to/template.html',
scope:
{
value: '=value',
maxValue: '=max-value'
}
};
})
.filter('range', function()
{
return function(input, total)
{
total = parseInt(total);
for (var i=1; i<=total; i++)
{
input.push(i);
}
return input;
};
});
In html part I have
<div>
<div ng-repeat="comment in car.comments">
Rating: <car-comment-raiting value="comment.rating" max-value="10"></car-comment-raiting>
</div>
</div>
template.html
<div>
<ul class="list-inline">
<li ng-repeat="n in [] | range:value"><span class="glyphicon glyphicon-star"></span></li>
</ul>
</div>
And I want to pass additional value to the template which should be calculated as maxValue - value. Haven't found any example describing that. Thought about using link property, but description tells, that it is used for other purpose.
UPD:
I was able to fix it with
return {
restrict: 'E',
templateUrl: 'path/to/template.html',
scope:
{
value: '=',
maxValue: '='
},
controller: function($scope)
{
$scope.calculated = $scope.maxValue - $scope.value;
}
};
but for some reason it doesn't work all the time. One time it works and the other time calculated variable is null.
All calculations must be done inside a direcitve link function or in controller.
Here is example with directive:
.directive('carCommentRaiting', function() {
return {
restrict: 'E',
template: 'path/to/template.html',
scope: {
value: '=value',
maxValue: '=max-value'
},
link : function(scope, element, attr) {
scope.calculated = scope.maxValue - scope.value;
/// watch value to update calculated on value update:
scope.$watch('value', function(newValue){
scope.calculated = scope.maxValue - newValue;
});
}
};
});

ng-hide doesn't work within hierarchy directives

I'm a newbie angular and will be happy to have some help here.
I'm struggeling to find out why I cannot setup one directive that is setting up an attribute hide="true" or "false" that will be used within the directive (Rank) as a parameter to change the inner directive (label) ng-hide to hide the label.
I tried everything
The outer directive (Rank) html:
<div>
<img src="/Components/Directives/images/blue_{{RankValue}}.svg" tooltip="{{RankValue}}/4" />
<label-info ng-hide="hide" header="{{header}}"></label-info>
</div>
The outer directive (Rank) directive java script:
angular.module('reusableDirectives')
.directive('Rank', function () {
return {
restrict: 'E',
scope: {
hide: '='
},
link: function (scope, element, attrs) {
scope.safeApply(scope.RankValue = scope.$eval(attrs.value));
scope.safeApply(scope.hidelabel = "true");
if (attrs.hidelabel == "false")
scope.safeApply(scope.hidelabel = "false");
scope.hidelabel = attrs.hide;
},
templateUrl: '/Components/Directives/Rank.html'
};
})
.controller('rankCtrl', ['scope', function ($scope) {
}]);
The inner directive (label) Html:
<script type="text/ng-template" id="myModalContent.html">
<div class="modal-header">
<h3>{{header}}</h3>
</div>
<div class="modal-body">
<div ng-bind-html="items"></div>
</div>
</div>
<div class="modal-footer">
<div style="float:left;">
<button class="btn btn-primary" ng-click="ok()">Close</button>
</div>
</div>
</script>
<div>
<div class="fs-labelInfo-text">
{{header}}
</div>
<img class="fs-labelInfo-img"
ng-click="update(header)"
src="Components/Directives/images/questionMark.png" />
</div>
The inner directive (Label) directive java script:
angular.module('reusableDirectives')
.directive('labelInfo', function () {
return {
restrict: 'E',
scope: {
isolatedLabelHide: '#hidelabel'
},
controller: function ($scope, $element, $modal, $log, $http, $rootScope, myService) {
$scope.header = "header attribute";
$scope.caption = "label caption";
$scope.ok = function (header) {
myService.getLabelInfo(header).then(function (data) {
//this will execute when the AJAX call completes.
$scope.items = data;
console.log(data);
$scope.open();
});
};
$scope.open = function () {
$log.info('open');
var modalInstance = $modal.open({
templateUrl: 'myModalContent.html',
controller: ModalInstanceCtrl,
resolve: {
header: function () {
return $scope.header;
},
items: function () {
return $scope.items;
}
}
});
modalInstance.result.then(function () {
}, function () {
$log.info('Modal dismissed at: ' + new Date());
});
};
},
link: function (scope, element, attrs) {
scope.header = attrs.header;
},
templateUrl: '/Components/Directives/LabelInfo.html'
};
});
angular.module('reusableDirectives')
.controller('ModalInstanceCtrl', function ($scope, $modalInstance, header, items) {
$scope.items = items;
$scope.header = header;
$scope.ok = function () {
$modalInstance.close();
};
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
};
});
The html that I'm using to test is:
One example to show the label:
<rank hide="false" value="3.5"></rank>
Another example to show the label:
<rank value="3.5"></rank>
example to hide will be:
<rank hide="true" value="3.5"></rank>
Thank you for your effort.
Best regards,
Chen
You set the scope property name as "hideLabel":
scope.hidelabel = attrs.hide;
So, you need to use "hideLabel" for the ng-hide attribute:
<label-info ng-hide="hideLabel" header="{{header}}"></label-info>
And you need to declare the ng-hide in your "labelInfo" directive template:
<div ng-hide="hideLabel">
(This "div" is the one that comes above <div class="fs-labelInfo-text"> at /Components/Directives/LabelInfo.html.)

dynamically adding directives in ng-repeat

I am trying to dynamically add different directives in an ng-repeat however the output is not being interpreted as directives.
I've added a simple example here: http://plnkr.co/edit/6pREpoqvmcnJJWzhZZKq
Controller:
$scope.colors = [{name:"red"}, {name: "blue"}, {name:"yellow"}];
Directive:
app.directive("red", function () {
return {
restrict: 'C',
template: "RED directive"
}
});
Html:
<ul>
<li ng-repeat="color in colors">
<span class="{{color.name}}"></span>
</li>
</ul>
How do I make angular pick up the directive specified in the class that is output via ng-repeat?
I know this is an old question, but google brought me here, and I didn't like the answers here... They seemed really complicated for something that should be simple. So I created this directive:
***** NEW CONTENT *****
I've since made this directive more generic, supporting a parsed (the typical angular value) "attributes" attribute.
/**
* Author: Eric Ferreira <http://stackoverflow.com/users/2954747/eric-ferreira> ©2016
*
* This directive takes an attribute object or string and adds it to the element
* before compilation is done. It doesn't remove any attributes, so all
* pre-added attributes will remain.
*
* #param {Object<String, String>?} attributes - object of attributes and values
*/
.directive('attributes', function attributesDirective($compile, $parse) {
'use strict';
return {
priority: 999,
terminal: true,
restrict: 'A',
compile: function attributesCompile() {
return function attributesLink($scope, element, attributes) {
function parseAttr(key, value) {
function convertToDashes(match) {
return match[0] + '-' + match[1].toLowerCase();
}
attributes.$set(key.replace(/([a-z][A-Z])/g, convertToDashes), value !== undefined && value !== null ? value : '');
}
var passedAttributes = $parse(attributes.attributes)($scope);
if (passedAttributes !== null && passedAttributes !== undefined) {
if (typeof passedAttributes === 'object') {
for (var subkey in passedAttributes) {
parseAttr(subkey, passedAttributes[subkey]);
}
} else if (typeof passedAttributes === 'string') {
parseAttr(passedAttributes, null);
}
}
$compile(element, null, 999)($scope);
};
}
};
});
For the OP's use case, you could do:
<li ng-repeat="color in colors">
<span attributes="{'class': color.name}"></span>
</li>
Or to use it as an attribute directive:
<li ng-repeat="color in colors">
<span attributes="color.name"></span>
</li>
***** END NEW CONTENT ******
/**
* Author: Eric Ferreira <http://stackoverflow.com/users/2954747/eric-ferreira> ©2015
*
* This directive will simply take a string directive name and do a simple compilation.
* For anything more complex, more work is needed.
*/
angular.module('attributes', [])
.directive('directive', function($compile, $interpolate) {
return {
template: '',
link: function($scope, element, attributes) {
element.append($compile('<div ' + attributes.directive + '></div>')($scope));
}
};
})
;
For the specific case in this question, one can just rewrite the directive a bit to make it apply the directive to a span by class, as so:
angular.module('attributes', [])
.directive('directive', function($compile, $interpolate) {
return {
template: '',
link: function($scope, element, attributes) {
element.replaceWith($compile('<span class=\"' + attributes.directive + '\"></span>')($scope));
}
};
})
;
Then you can use this anywhere and select a directive by name dynamically. Use it like so:
<li ng-repeat="color in colors">
<span directive="{{color.name}}"></span>
</li>
I purposely kept this directive simple and straightforward. You may (and probably will) have to reword it to fit your needs.
i faced with the same problem in one of my projects and you can see how i solve this problem on jsfiddle
HTML:
<div class="page-wrapper" ng-controller="mainCtrl">
<div class="page">
<h3>Page</h3>
<ul>
<li ng-repeat="widget in widgets"><div proxy="widget" proxy-value="{{widget}}"></div></li>
</ul>
</div>
JS:
var app = angular.module('app',[]);
app.controller('mainCtrl', ['$scope', '$q', 'widgetAPI', function($scope, $q, widgetAPI) {
$scope.widgets = [];
widgetAPI.get().then(
function(data) {
$scope.widgets = data;
},
function(err) {
console.log("error", err);
}
);}])
.service('widgetAPI', ['$q', function($q) {
var api = {};
api.get = function() {
//here will be $http in real app
return $q.when(
[
{
component: 'wgtitle',
title: "Hello world",
color: '#DB1F1F',
backgroundColor: '#c1c1c1',
fontSize: '32px'
},
{
component: 'wgimage',
src: "http://cs425622.vk.me/v425622209/79c5/JgEUtAic8QA.jpg",
width: '100px'
},
{
component: 'wgimage',
src: "http://cs425622.vk.me/v425622209/79cf/S5F71ZMh8d0.jpg",
width: '400px'
}
]
);
};
return api;}])
.directive('proxy', ['$parse', '$injector', '$compile', function ($parse, $injector, $compile) {
return {
replace: true,
link: function (scope, element, attrs) {
var nameGetter = $parse(attrs.proxy);
var name = nameGetter(scope);
var value = undefined;
if (attrs.proxyValue) {
var valueGetter = $parse(attrs.proxyValue);
value = valueGetter(scope);
}
var directive = $injector.get(name.component + 'Directive')[0];
if (value !== undefined) {
attrs[name.component] = value;
}
var a = $compile(directive.template)(scope);
element.replaceWith(a);
}
}}])
.directive('wgtitle', function() {
return {
restrict: 'A',
scope: true,
replace: true,
template: '<h1 style="color:{{widget.color}}; font-size:{{widget.fontSize}}; background:{{widget.backgroundColor}}" >{{widget.title}}</h1>',
link: function(scope, element, attrs) {
}
}})
.directive('wgimage', function() {
return {
restrict: 'A',
scope: true,
replace: true,
template: '<img style="width:{{widget.width}}" src="{{widget.src}}"/>',
link: function(scope, element, attrs) {
}
}});
I hope it will usefull.
I don't think you'll be able to just assign the directive as a class name - you would need to run this through $compile again, which would be heading down the path towards recursion errors.
One possible solution is outlined at: AngularJS - how to have a directive with a dynamic sub-directive
If it works for your use case, you can use templates instead:
<div ng-repeat='template in inner' ng-include='template'></div>

Resources