Initializing an Angular Directive in JavaScript - angularjs

I have a directive in my template. It's working great:
<ul class="activity-stream">
<my-activity-stream-item ng-repeat="activity in vm.activities" activity="activity"></my-activity-stream-item>
</ul>
I'd basically like to include the same HTML in that template as a popup in a Leaflet Map, but I have no idea how to create that in code. Here's what I tried:
for (i = 0; i < activities.length; i++) {
var activity = activities[i];
var marker = L.marker([activity.location.lat, activity.location.lng]);
marker.type = activity.type;
marker.bindPopup( '<my-activity-stream-item activity="activity"></my-activity-stream-item>' );
marker.addTo( map );
}
I didn't really expect that to work, I feel like I have to pass the scope in somehow... but I'm at a complete loss as to how to do it.
var app = angular.module('myPortal');
app.factory('TemplateService', TemplateService);
app.directive('myActivityStreamItem', myActivityStreamItem);
function myActivityStreamItem( $compile, TemplateService ) {
return {
restrict: 'E',
link: linker,
transclude: true,
scope: {
activity: '='
}
};
function linker(scope, element, attrs) {
scope.rootDirectory = 'images/';
TemplateService.getTemplate( 'activity-' + scope.activity.type ).then(function(response) {
element.html( response.data );
$compile(element.contents())(scope);
});
}
}
function TemplateService( $http ) {
return {
getTemplate: getTemplate
};
function getTemplate( templateName ) {
return $http.get('/templates/' + templateName + '.html');
}
}
(Note - I've only been using Angular for about a week, so please let me know if you think I've done this completely wrong)
EDIT: I took Chandermani's advice and switched my directive to an ngInclude:
<ul class="activity-stream">
<li ng-repeat="activity in vm.activities" ng-include="'/templates/activity-' + activity.type + '.html'"></li>
</ul>
This works great! I also tried to use Josh's advice to compile the HTML in JavaScript, however I'm not quite there...
var link = $compile('<li ng-include="\'/templates/activity-' + activity.type + '.html\'"></li>');
var newScope = $rootScope.$new();
newScope.activity = activity;
var html = link( newScope );
marker.bindPopup( html[0] );
This results in the popup appearing, but the HTML contained within the popup is a comment: <!-- ngInclude: '/templates/activity-incident.html' -->
Do I have to pass it the activity in the li somehow?
Edit 2: Got it! As noted in Issue #4505, you need to wrap the snippet in something, so I wrapped my ngInclude in a div:
var link = $compile( '<div><ng-include src="\'/templates/activity-incident.html\'"></ng-include></div>' );

Not sure i have understood your problem, but what you can do is to use ng-include directive and it can take a template expression to dynamically load a template. Something like:
<ul class="activity-stream">
<li ng-repeat="activity in vm.activities" ng-include="'/templates/activity-' + activity.type + '.html'"></li>
</ul>
You may not require a directive here.

Anytime you want to add raw HTML to the page and have Angular process it, you need to use the $compile service.
Calling $compile on a template will return a linking function which can then be used to bind a scope object to.
var link = $compile('<span>{{someObj}}</span>');
Linking that function to a scope object will result in an element that can then be appended into the DOM.
//Or the scope provided by a directive, etc...
var newScope = $rootScope.$new();
var elem = link(newScope);
//Could also be the element provided by directive
$('someSelector').append(elem);
That's the basic flow you need to be able to tell Angular to process your DOM element. Usually this is done via a directive, and that's probably what you need in this case as well.

Related

How to make Angular 1.5 understand a expression from HTML string added via DOM in runtime [duplicate]

Im trying to put some angular js template string inside an element, and expect an complied output. But that's not happening.
HTML
<div ng-controller="testController">
<div ng-bind-html-unsafe="fruitsView"></div>
</div>
Controller:
function filterController($scope){
...
$scope.arr = ["APPLE", "BANANA"];
$scope.fruitsView = '<div><p ng-repeat="each in arr">{{each}}</p></div>';
}
The ouput is just {{each}}.
So how do i insert an angular js template string (here $scope.fruitsView) inside an element?
I have made a fiddle for this.
In this case, you don't want to just "insert HTML", but compile it. You can create DOM nodes using the $compile service.
var tpl = $compile( '<div><p ng-repeat="each in arr">{{each}}</p></div>' )( scope );
As you can see, $compile returns a function that takes a scope object as a parameter, against which the code is evaluated. The resultant content can be inserted into the DOM with element.append(), for example.
Important note: But under no circumstances does any DOM-related code belong in your controller. The proper place is always a directive. This code can easily be thrown into a directive, but I wonder why you are programmatically inserting the HTML at all.
Can you shed some light here so I can provide a more specific answer?
Update
Assuming your data comes from a service:
.factory( 'myDataService', function () {
return function () {
// obviously would be $http
return [ "Apple", "Banana", "Orange" ];
};
});
And your template comes from a service
.factory( 'myTplService', function () {
return function () {
// obviously would be $http
return '<div><p ng-repeat="item in items">{{item}}</p></div>';
};
});
Then you create a simple directive that reads in the provided template, compiles it, and adds it to the display:
.directive( 'showData', function ( $compile ) {
return {
scope: true,
link: function ( scope, element, attrs ) {
var el;
attrs.$observe( 'template', function ( tpl ) {
if ( angular.isDefined( tpl ) ) {
// compile the provided template against the current scope
el = $compile( tpl )( scope );
// stupid way of emptying the element
element.html("");
// add the template content
element.append( el );
}
});
}
};
});
Then from your view:
<div ng-controller="MyCtrl">
<button ng-click="showContent()">Show the Content</button>
<div show-data template="{{template}}"></div>
</div>
And in the controller, you simply tie it together:
.controller( 'MyCtrl', function ( $scope, myDataService, myTplService ) {
$scope.showContent = function () {
$scope.items = myDataService(); // <- should be communicated to directive better
$scope.template = myTplService();
};
});
And it should all work together!
PS: this is all assuming your template comes from the server. If it doesn't, then your template should be in the directive, which simplifies things.

Modifying styles of a compiled html element in Angularjs

UPDATE
I think I found the issue, the template variable is lossing it's value, I don't get why yet, I've changed the code a bit:
var template;
$templateRequest("ng-templates/app/cart-counter.html").then(function(html){
template = angular.element(html);
element.append(template);
$compile(template)(scope);
console.log("template: " + template); // This returns the template object
});
var unbindWatcher = scope.$watch(
"clickCounter",
function(newClickCounter){
console.log("template: " + template); // This returns undefined
if (newClickCounter >= 5) {
var cartButton = this.template.children('.btn');
cartButton.toggleClass('btn-success'); // this throws undefined error
unbindWatcher();
}
}
);
My question now would be why is the template variable undefined when it had a value earlier and what should I do to fix it?
ORIGINAL QUESTION
I am playing around with Angular, trying to change some elements classes by compiling an html adding it to the DOM and when an event happens, I am trying to use angularElement to access the childs of the html I compiled and toggling some classes.
This is not giving me an error, but the changes in the classes are not happening and I can't find what Im doing wrong, please help.
This is the code for the directive:
store.directive("appCartCounter", ['$templateRequest', '$compile', function($templateRequest, $compile){
var link = function(scope, element){
this.messages = [
"Sorry, the shopping cart is not implemented",
"Hey, I told you, it's not ready",
"Stop that! It's anoying",
"I'm getting really really angry",
"YEarghhh!!!!"
];
scope.messages = this.messsages;
scope.clickCounter = 0;
scope.incrementCount = function(){
scope.clickCounter++;
};
$templateRequest("ng-templates/app/cart-counter.html").then(function(html){
this.template = angular.element(html);
element.append(template);
$compile(template)(scope);
});
var unbindWatcher = scope.$watch(
"clickCounter",
function(newClickCounter){
console.log("I've been watching you... alalalong");
if (newClickCounter >= 5) {
var cartButton = this.template.children('.btn');
var messageElement = this.template.children('.text-info');
cartButton.toggleClass('btn-success');
cartButton.toggleClass('btn-danger');
cartButton.toggleClass('btn-lg');
messageElement.toggleClass('text-info');
messageElement.toggleClass('text-danger');
messageElement.toggleClass('text-capitalize');
messageElement.toggleClass('lead');
unbindWatcher();
console.log("I'm blind!!");
}
}
);
};
return {
restrict: 'E',
scope: {
'addedProducts' : '='
},
replace: 'true',
link: link
};
}]);
cart-counter.html:
<button
class="btn btn-success"
data-ng-show="addedProducts.length"
data-ng-click="incrementCount()"
data-ng-cloak>
<i class="glyphicon glyphicon-shopping-cart"></i> {{addedProducts.length}}
</button>
<p data-ng-show="clickCounter" class="text-info">
{{messages[clickCounter]}}
</p>
html using the directive:
<app-cart-counter data-added-products="storeCtrl.addedProducts"></app-cart-counter>
I'll try to get a simpler example in a plunker.
Thanks!!
In the end I managed to "fix" the problem by saving the template to the scope, altough I still don't understand why the variable or the saving it to "this." didn't worked.
As a sidenote, selecting the children via tagname didn't worked, I also tried with the children number and classname (even though I have jquery imported in the solution). To solve that, I had to access the elements via the template object's array, and wrap that in an angular.element(), eg:
var cartButton = angular.element(scope.template[0]);
var messageElement = angular.element(scope.template[2]);
PS: scope.template[1] returned me a text node because of the linebreak, I hadn't expected that.

Create new isolated scope programmatically

I am trying to create popup html to go into a leafletjs marker popup.
I have the following partial:
<div class="popup">
<div class="pull-right">
<a class="popup-info" ng-click="onInfoClicked()">
<i class="fa fa-info-circle"></i>
</a>
</div>
<div class="popup-title">
{{title}}
</div>
<div class="popup-subtitle">
{{subtitle}}
</div>
</div>
and the following directive:
app.directive('leafletPopup', function() {
return {
restrict: "A",
templateUrl: "app/partials/leaflet-popup.html",
scope: {
feature: '=',
popupInfo: '&'
},
controller: function ($scope) {
$scope.title = $scope.feature.properties.title;
$scope.subtitle = $scope.feature.properties.description;
$scope.onInfoClicked = function() {
$scope.popupInfo({feature: feature});
}
}
};
});
I have a controller that provides a function to generate the html for each marker that I am going to place on the map:
function html(feature) {
var el = angular.element('<div leaflet-popup="feature" popup-info="onPopupInfo(feature)"></div>');
var compiled = $compile(el);
var newScope = $scope.$new(true); // create new isolate scope
newScope.feature = feature; // inject feature into the scope
newScope.onPopupInfo = function(feature) { // inject function into scope
// do something with the click
showFeatureDetails(feature);
}
compiled(newScope);
return el[0];
}
Works perfectly. Great right?
I have read in a couple of places that its not recommended to create your own scope manually as you have to make sure you also destroy it manually.
Lets say I have a bunch of markers on my map, and each has a popup. Do I need to track all the new scopes I create in my controller and call destroy on them? When? Only if my marker gets removed from the map?
I could just skip angular and build the entire html element with jquery and attach the onclick to a function, but that is also not pretty. And why skip angular?
Seems overly complicated, which probably means I am doing this the hard way ;)

AngularJS Template Replace directive not working

I have a templating directive that is almost working the way I want, simple version works :-
<div bw-template-replace>
<template>This is template text, replace [[#this]] please</template>
<this>text replaced</this>
</div>
expands to
<div bw-template-replace><span>This is template text, replace text replaced please</span></div>
However, if I embed other directives they dont fully work as expected.
See my plunk http://plnkr.co/edit/dLUU2CMtuN5WMZlEScQi?p=preview
At the end of the directive's link function I $compile the resulting text/node which works for {{scope interpolated text}} but does not work for embedded directives using the same scope.
The reason I need this is because I am using ng-translate for an existing ng-app and I want to use the existing English text as keys for translation lookups. An uncommon translation case is where we have HTML like the following (extract from the app), the [[#ageInput]] and [[#agePeriod]] 'arguments' may appear at different places in other languages, as I understand it ng-translate has no real support for this scenario currently.
<div class="row-fluid">
<div class="span12" translate bw-template-replace>
<template>
If current version of media has not been read for [[#ageInput]] [[#agePeriod]]
</template>
<ageInput><input type=number ng-model="policy.age" style="width:50px"/></ageInput>
<agePeriod><select style="width:100px" ng-model="policy.period" ng-options="p for p in periods" /></agePeriod>
</div>
</div>
Any help much appreciated.
I love having to work through these scenarios when you are a newbie at something as it really forces you to understand what is happening. I now have it working, basically my previous directive I found several ways of just replacing the html in the hope Angular would magically sort everything out. Now I have a better understanding of transclusion and in particular the transclusion function I made it work as desired. The cloned element passed into $transcludeFn already has scope attached and has been $compiled, so my function now parses the template text and generates individual textElement's and moves the argument elements around to suit the template.
My Current Solution
.directive('TemplateReplace', ['$compile', '$document', '$timeout',
function ($compile, $document, $timeout) {
return {
restrict: 'AC',
transclude: true,
link: function (scope, iElement, iAttrs, controller, transclude) {
transclude(scope, function (clone, $scope) {
$timeout(function () {
// Our template is the first real child element (nodeType 1)
var template = null;
for (var i = 0, ii = clone.length; i < ii; i++) {
if (clone[i].nodeType == 1) {
template = angular.element(clone[i]);
break;
}
}
// Remember the template's text, then transclude it and empty its contents
var html = angular.copy(template.text());
iElement.append(template); // Transcluding keeps external directives intact
template.empty(); // We can populate its inards from scratch
// Split the html into pieces seperated by [[#tagname]] parts
if (html) {
var htmlLen = html.length;
var textStart = 0;
while (textStart < htmlLen) {
var tagName = null,
tagEnd = htmlLen,
textEnd = htmlLen;
var tagStart = html.indexOf("[[#", textStart);
if (tagStart >= 0) {
tagEnd = html.indexOf("]]", tagStart);
if (tagEnd >= 0) {
tagName = html.substr(tagStart + 3, tagEnd - tagStart - 3);
tagEnd += 2;
textEnd = tagStart;
}
}
// Text parts have to be created, $compiled and appended
var text = html.substr(textStart, textEnd - textStart);
if (text.length) {
var textNode = $document[0].createTextNode(text);
template.append($compile(textNode)($scope));
}
// Tag parts are located in the clone then transclude appended to keep external directives intact (note each tagNode can only be referenced once)
if (tagName && tagName.length) {
var tagNode = clone.filter(tagName);
if (tagNode.length) {
template.append(tagNode);
}
}
textStart = tagEnd;
}
}
}, 0);
});
}
};
}
]);
If I have understood your question properly, I guess the issue is your directive is getting executed before ng-repeat prepares dom.So you need to do DOM manipulation in $timeout.
Check this plunker for working example.
Here is a nice explanation of similar problem: https://stackoverflow.com/a/24638881/3292746

Angular dynamically created directive not executing

Plnkr sample: [http://plnkr.co/edit/jlMQ66eBlzaNSd9ZqJ4m?p=preview][1]
This might not be the proper "Angular" way to accomplish this, but unfortunately I'm working with some 3rd party libraries that I have limited ability to change. I'm trying to dynamically create a angular directive and add it to the page. The process works, at least in the sense where the directive element gets added to the DOM, HOWEVER it is not actually executed - it is just a dumb DOM at this point.
The relevant code is below:
<div ng-app="myModule">
<div dr-test="Static Test Works"></div>
<div id="holder"></div>
<a href="#" onclick="addDirective('Dynamic test works')">Add Directive</a>
</div>
var myModule = angular.module('myModule', []);
myModule.directive('drTest', function () {
console.log("Directive factory was executed");
return {
restrict: 'A',
replace: true,
link: function postLink(scope, element, attrs) {
console.log("Directive was linked");
$(element).html(attrs.drTest);
}
}
});
function addDirective(text){
console.log("Dynamically adding directive");
angular.injector(['ng']).invoke(['$compile', '$rootScope',function(compile, rootScope){
var scope = rootScope.$new();
var result = compile("<div dr-test='"+text+"'></div>")(scope);
scope.$digest();
angular.element(document.getElementById("holder")).append(result);
}]);
}
</script>
While appending the directive to DOM you need to invoke with your module as well in the injector, because the directive drTest is available only under your module, so while creating the injector apart from adding ng add your module as well. And you don't really need to do a scope apply since the element is already compile with the scope. You can also remove the redundant $(element).
angular.injector(['ng', 'myModule']).invoke(['$compile', '$rootScope',function(compile, rootScope){
var scope = rootScope.$new();
var result = compile("<div dr-test='"+text+"'></div>")(scope);
angular.element(document.getElementById("holder")).append(result);
}]);
Demo

Resources