Add dynamic transcluded content - angularjs

I'm trying to accomplish the following scenario:
I've got the following directive:
return {
restrict: "AE",
replace: true,
transclude: true,
scope: {
chartData: "=",
groupId: "#",
groupName: "#"
},
template: "<div class=\"flex\" ng-transclude></div>",
compile: function(){
var charts = [];
return function (scope, element, attributes, controller, transcludeFn) {
_.forEach(scope.chartData, function (data) {
var id = "gauge-" + scope.groupId + '-' + data.name;
scope[id] = data.value; // gauge directive can now access this value
element.append("<div class=\"flex-spacer flex-column\"><div bv-gauge chart-title=\"" + data.name + "\" data-value=\"" + scope[id] + "\" series-name=\"" + scope.groupName + "\"></div></div>");
charts.push(id);
});
$compile(element.contents())(scope);
scope.$watch("chartData", function (newVal) {
_.forEach(scope.chartData, function (data) {
var id = "gauge-" + scope.groupName + '-' + data.name;
scope.$broadcast("chart:update", newVal);
});
});
}
}
};
As you can see I'm adding dynamically some content to the directive's element. It works nice but there is one issue; the isolated scope of inner directives ( i.e. bv-gauge) can see the outer scope only not the scope of bv-gauge-parent; As far as I know this is the default behaviour, which we can override by using the transclude function.
However, it seems that angular doesn't consider the dynamically added content as part of the transcluded content. So when I do the following:
transcludeFn(scope, function(clone, scope){
// clone doesn't have the content we added earlier
});
Is there any way to associate the scope of the transcluded content with the parent directive?

Related

How to make directive use the controller specified in directive attribute?

So I have a directive:
<directive data="user" templateUrl="./user.html" controller="UserController"></directive>
I want that directive to use the controller specified in "controller" attribute, as you see above.
Is it possible with AngularJS directives? Or should I do it other way, maybe with components?
My code currently looks like this:
app.directive('directive', function() {
var controllerName = "UserController"; // i want that to dynamicaly come from attribute
// check if controller extists:
var services = [];
app['_invokeQueue'].forEach(function(value){
services[value[2][0]] = true;
});
if (!services[controllerName]) controllerName = false;
return {
scope: { 'data' : '=' },
link: function (scope) {
Object.assign(scope, scope.data);
},
templateUrl: function(element, attr) {
return attr.templateurl;
},
controller: controllerName
}
});
You can do following (not exactly what you ask - it creates bunch of nested scopes, but should be sufficient):
.directive('directive', () => {
scope: { 'data' : '=' },
template: (elem, attrs) => {
return '<div ng-controller="' + attrs.controller + ' as vm"><div ng-include="' + attrs.template + '"></div></div>';
}
});
<directive data="user" templateUrl="./user.html" controller="UserController"></directive>
you may use $templateCache directly instead of ng-include
if you need controller/template/... to be dynamic, you need to observe/watch + dom manipulation + recompile stuff
Okay, so after analysing Petr's answer I post the working code using nested divs:
app.directive('directive', function() {
return {
scope: { 'data' : '=' },
link: function (scope) {
// this makes your fields available as {{name}} instead of {{user.name}}:
Object.assign(scope, scope.data);
},
template: function(element, attrs) {
var controllerName = attrs.controller;
var controllerString = controllerName + ' as vm';
// check if controller extists:
var services = [];
app['_invokeQueue'].forEach(function(value){
services[value[2][0]] = true;
})
if (!services[controllerName]) {
return '<div ng-include="\'' + attrs.templateurl + '\'"></div>';
} else {
return '<div ng-controller="' + controllerString + '"><div ng-include="\'' + attrs.templateurl + '\'"></div></div>';
}
}
}
});

Testing angularjs directive generating html code or not

I have a directive something like this which I want to test,
.directive('gridHeader', function() {
return {
restrict: 'A',
replace: true,
scope: false,
compile: function(tEle, tAttrs, transcludeFn) {
var h3 = tEle.find('h3');
var temp = h3.html();
temp = temp.replace('xxxx', tAttrs.gridHeader);
h3.html(temp);
},
template: '<div class="grid-header">' +
'<h3>Showing {{grid.data.records}} xxxx</h3>' +
'<div class="pull-right">' +
'</div>' +
'<div class="clearfix"></div>' +
'</div>'
}
})
I have tried something like this, but its not working
it('directive: should generate all required html elements', function() {
var items = angular.element('div.grid-header');
console.log(items.length); //always returning 0
expect(true).toBe(true);
});
I'm not a fan of checking directive HTML in unit tests (I feel that's better suited in e2e) however something like this should suffice...
Save a reference to the compiled element
var element;
...
inject(function($rootScope, $compile) {
var scope = $rootScope.$new();
scope.grid = { data: { records: 'records' } };
element = $compile('<div grid-header="foo"></div>')(scope);
$rootScope.$digest();
});
Then you can run tests against it
it('whatever', function() {
expect(element.hasClass('grid-header')).toBeTruthy();
expect(element.find('h3').text()).toEqual('Showing records foo');
});

Update ng-options hosted by a directive

I have a custom directive that displays a dropdownlist.
Is it possible to dynamically re-populate the ng-options from a datasource that comes from the controller that hosts the directive.
The datasource itself comes from a service.
Currently it works well from the initial array passed to the directive, but when I add new data (from the controller/service to this array I would like to update the item list.
Any help?
EDIT :
This is how I use my directive.
<select-item-obj-from-array datasource="ctrl.ActivityAddresses" ng-model="form.Activity.AddressID" name="AddressID" value="AddressID" label="City" .... />
My directive looks like:
app.directive('selectItemObjFromArray', function () {
return {
restrict: 'E',
replace: true,
template: function (element, attrs) {
var tpl = '';
tpl += "<div><div class=\"form-group clearfix\" >";
tpl += '<label for="' + attrs.name + '" class="col-lg-3 control-label">' + attrs.label + '</label>';
tpl += '<div class="col-lg-9">';
tpl += '<select ng-disabled="ngDisabled" name="' + attrs.name + '" ng-model="ngModel" chosen="datasource" ng-options="c.Name for c in datasource"></select>';
tpl += '</div>';
tpl += '</div>';
tpl += '</div>';
return tpl;
},
scope: {
ngModel: "=",
datasource: "="
},
link: function (scope, elem, attrs) {
var select = elem.find("select").eq(0);
select.chosen();
scope.$watch(function () {
return select[0].length;
},
function (newvalue, oldvalue) {
if (newvalue !== oldvalue) {
select.trigger("chosen:updated");
}
});
scope.$watch(attrs.ngModel, function () {
select.trigger('chosen:updated');
});
}
};
});
if my controller/service updated the ctrl.ActivityAddresses I don't know how to "reinvoke" the directive to update the dropdownlist..
You can broadcast from your service like this:
var broadcast = function() {
$rootScope.$broadcast('items.update');
};
Assuming that items is an array.
Then you can catch the broadcast in your controller or directive:
$scope.$on('items.update', function (event) {
//Do whatever you want with the items.
});
I think this is what you want? You don't need to change the ng-options directive for this.

Calling element.replaceWith from inside scope.$on throws TypeError

We have the following directive:
.directive("directiveToggleElement", function(FlagsService) {
return {
restrict: "E",
scope: {},
link: function(scope, element, attrs)
{
scope.showMe = FlagsService.isOn(attrs.toggleKey);
scope.featureText = attrs.featureText;
var htmlTag = '<div class="panel ng-scope" ng-class="{selected: selected}" ng-click="selected = !selected;"></div>';
var htmlComment = '<!-- TogglePlaceholder: ' + attrs.toggleKey +'-->';
element.replaceWith(scope.showMe ? htmlComment + htmlTag : htmlComment);
// Listen for change broadcast from flagsservice to toggle local store
scope.$on('LocalStorage.ToggleChangeEvent.' + attrs.toggleKey, function(event, parameters) {
console.log('stuff changed - ' + parameters.key + ' ' + parameters.newValue);
scope.showMe = parameters.newValue;
element.replaceWith(scope.showMe ? htmlComment + htmlTag : htmlComment);
});
}
}
})
The idea is that based on the value of a feature toggle (or feature flag) the directive will output the comment with the tag, or just the comment.
The initial element.replaceWith works as expected, but the call from inside the scope.$on generates a "TypeError: Cannot call method 'replaceChild' of null". We've inspected element before each call and can't spot an obvious difference.
Can anyone explain why the error would be thrown here, or advise of a potential workaround that will allow us to do the same thing?
We have a working directive that sets the value of ng-show:
.directive("directiveToggle", function(FlagsService) {
return {
restrict: "A",
transclude: true,
scope: {},
link: function(scope, element, attrs)
{
scope.showMe = FlagsService.isOn(attrs.toggleKey);
// Listen for change broadcast from flagsservice to toggle local store
scope.$on('LocalStorage.ToggleChangeEvent.' + attrs.toggleKey, function(event, parameters) {
console.log('stuff changed - ' + parameters.key + ' ' + parameters.newValue);
scope.showMe = parameters.newValue;
});
},
template: '<div class="panel ng-scope" ng-show="showMe" ng-class="{selected: selected}" ng-click="selected = !selected;"><span ng-transclude></span></div>',
replace: true
}
})
But we would prefer to remove the element from the DOM instead of setting display to none.
You could still use the ng-if directive within your custom "directiveToggle" without the need for a controller. To achieve this you need to use the transclude option:
.directive("directiveToggle", function (FlagsService) {
return {
template: '<div ng-transclude ng-if="isEnabled"></div>',
transclude: true,
restrict: 'A',
scope: true,
link: function ($scope, $element, $attrs) {
var feature = $attrs.featureToggle;
$scope.isEnabled = FlagsService.isOn(feature);
$scope.$on('LocalStorage.ToggleChangeEvent.' + feature, function (event, parameters) {
$scope.isEnabled = parameters.newValue;
});
}
}
});
The above is taking the mark-up from inside the "directiveToggle" and then wrapping it inside of the template, where the "ng-transclude" directive marks the insertion point. Also included on the template is an "ng-if" directive that is watching the "isEnabled" member on the current scope for the "directiveTemplate".
One caveat with this approach is that you need the directive to create it's own scope/isolated scope if you have multiple instances of this directive, otherwise the "isEnabled" member will be shared between the directives.

How to create a directive with a dynamic template in AngularJS?

How can I create a directive with a dynamic template?
'use strict';
app.directive('ngFormField', function($compile) {
return {
transclude: true,
scope: {
label: '#'
},
template: '<label for="user_email">{{label}}</label>',
// append
replace: true,
// attribute restriction
restrict: 'E',
// linking method
link: function($scope, element, attrs) {
switch (attrs['type']) {
case "text":
// append input field to "template"
case "select":
// append select dropdown to "template"
}
}
}
});
<ng-form-field label="First Name" type="text"></ng-form-field>
This is what I have right now, and it is displaying the label correctly. However, I'm not sure on how to append additional HTML to the template. Or combining 2 templates into 1.
i've used the $templateCache to accomplish something similar. i put several ng-templates in a single html file, which i reference using the directive's templateUrl. that ensures the html is available to the template cache. then i can simply select by id to get the ng-template i want.
template.html:
<script type="text/ng-template" id=“foo”>
foo
</script>
<script type="text/ng-template" id=“bar”>
bar
</script>
directive:
myapp.directive(‘foobardirective’, ['$compile', '$templateCache', function ($compile, $templateCache) {
var getTemplate = function(data) {
// use data to determine which template to use
var templateid = 'foo';
var template = $templateCache.get(templateid);
return template;
}
return {
templateUrl: 'views/partials/template.html',
scope: {data: '='},
restrict: 'E',
link: function(scope, element) {
var template = getTemplate(scope.data);
element.html(template);
$compile(element.contents())(scope);
}
};
}]);
Had a similar need. $compile does the job. (Not completely sure if this is "THE" way to do it, still working my way through angular)
http://jsbin.com/ebuhuv/7/edit - my exploration test.
One thing to note (per my example), one of my requirements was that the template would change based on a type attribute once you clicked save, and the templates were very different. So though, you get the data binding, if need a new template in there, you will have to recompile.
You should move your switch into the template by using the 'ng-switch' directive:
module.directive('testForm', function() {
return {
restrict: 'E',
controllerAs: 'form',
controller: function ($scope) {
console.log("Form controller initialization");
var self = this;
this.fields = {};
this.addField = function(field) {
console.log("New field: ", field);
self.fields[field.name] = field;
};
}
}
});
module.directive('formField', function () {
return {
require: "^testForm",
template:
'<div ng-switch="field.fieldType">' +
' <span>{{title}}:</span>' +
' <input' +
' ng-switch-when="text"' +
' name="{{field.name}}"' +
' type="text"' +
' ng-model="field.value"' +
' />' +
' <select' +
' ng-switch-when="select"' +
' name="{{field.name}}"' +
' ng-model="field.value"' +
' ng-options="option for option in options">' +
' <option value=""></option>' +
' </select>' +
'</div>',
restrict: 'E',
replace: true,
scope: {
fieldType: "#",
title: "#",
name: "#",
value: "#",
options: "=",
},
link: function($scope, $element, $attrs, form) {
$scope.field = $scope;
form.addField($scope);
}
};
});
It can be use like this:
<test-form>
<div>
User '{{!form.fields.email.value}}' will be a {{!form.fields.role.value}}
</div>
<form-field title="Email" name="email" field-type="text" value="me#example.com"></form-field>
<form-field title="Role" name="role" field-type="select" options="['Cook', 'Eater']"></form-field>
<form-field title="Sex" name="sex" field-type="select" options="['Awesome', 'So-so', 'awful']"></form-field>
</test-form>
One way is using a template function in your directive:
...
template: function(tElem, tAttrs){
return '<div ng-include="' + tAttrs.template + '" />';
}
...
If you want to use AngularJs Directive with dynamic template, you can use those answers,But here is more professional and legal syntax of it.You can use templateUrl not only with single value.You can use it as a function,which returns a value as url.That function has some arguments,which you can use.
http://www.w3docs.com/snippets/angularjs/dynamically-change-template-url-in-angularjs-directives.html
I managed to deal with this problem. Below is the link :
https://github.com/nakosung/ng-dynamic-template-example
with the specific file being:
https://github.com/nakosung/ng-dynamic-template-example/blob/master/src/main.coffee
dynamicTemplate directive hosts dynamic template which is passed within scope and hosted element acts like other native angular elements.
scope.template = '< div ng-controller="SomeUberCtrl">rocks< /div>'
I have been in the same situation, my complete solution has been posted here
Basically I load a template in the directive in this way
var tpl = '' +
<div ng-if="maxLength"
ng-include="\'length.tpl.html\'">
</div>' +
'<div ng-if="required"
ng-include="\'required.tpl.html\'">
</div>';
then according to the value of maxLength and required I can dynamically load one of the 2 templates, only one of them at a time is shown if necessary.
I heope it helps.

Resources