Render a custom directive when loaded dynamically from controller - angularjs

I have three custom directive, say <sample-grid></sample-grid> , <sample-form></sample-form> and <sample-view></sample-view>. These are defined in the following way:
angular.module("MyApp").directive('sampleGrid', [ function() {
return {
scope : {},
restrict: 'EA',
templateUrl : 'view/templates/sample-grid.tmpl.html',
controller : 'SampleGridCtrl',
scope: {
scnid: '=',
}
};
} ]);
I have a main view where I am using <md-tabs> from angular material in following way -
<md-tabs>
<md-tab ng-repeat="tab in myCtrl.pageTabs track by $index">
<md-tab-label>{{tab.tabName}}</md-tab-label>
<md-tab-body>
<div id="tab.tabName" ng-bind-html="tab.tabHTML"></div>
</md-tab-body>
</md-tab>
</md-tabs>
also the controller for this view defined as
angular.module("MyApp").controller("myCtrl",['$log','$scope','$sce', '$compile', function myCtrl ($log,$scope,$sce,$compile) {
var homeScope = this;
homeScope.pageTabs = [];
var newTab = {};
newTab.tabName = "Sample";
newTab.tabHTML = $sce.trustAsHtml("<sample-grid></sample-grid>");
newTab.tabAction = "grid";
homeScope.pageTabs.push(newTab);
var template = angular.element(document.querySelector('#Sample'));
$compile(template)(homeScope);
}])
The requirement is: user will be able to add new tabs in view, and I have to show one of the three custom directives in the view based on the selection. So I have to just push new tab objects into homeScope.pageTabs with the required property.
The problem is : After pushing the tab object, new tab is created but the custom directive does not render into view. I can see it in the console like :
<div id="tab.tabName" ng-bind-html="tab.tabHTML">
<sample-grid></sample-grid>
</div>
I have seen answers here such that I have to use $compile to render it correctly. I tried that, But I could not get the proper solution.
So my question is how I can achieve this functionality? And, is there any other easy and possible way to achieve this?

I have changed your controller logic a little bit. You have to use compile a little bit different.
angular.module("MyApp").controller("myCtrl",['$log','$scope','$sce', '$compile', function myCtrl ($log,$scope,$sce,$compile) {
var homeScope = this;
homeScope.pageTabs = [];
var template = $sce.trustAsHtml("<sample-grid></sample-grid>");
var linkFn = $compile(template);
var content = linkFn(scope);
var newTab = {};
newTab.tabName = "Sample";
newTab.tabHTML = content;
newTab.tabAction = "grid";
homeScope.pageTabs.push(newTab);
}])

Related

ngRepeat error when removing child directive from parent directive

I´m having some issues with a gallery manager component where you can add/remove the pictures.
This is the html code of the gallery handler:
<img src = '{{snapshot}}' >
<div class = 'md-button l3' is-file ng-model = 'picture' blob-url = 'snapshot'>Upload</div>
<div class = 'md-button l3' ng-click = 'article.pictures.push(picture)'>Add</div>
<div gallery-manager = 'article.pictures'></div>
Below, the directives:
.directive("galleryManager", function($compile){
var controllerFn = function($scope, $element, $attrs){
var self = this;
self.removeItemAt = function($index){
self.pictures.splice($index, 1);
$compile($element)($scope); <--Note this
}
}
var linkFn = function($scope, $element, $attrs){
}
return {
template:"<div gallery-item = 'picture' ng-repeat = 'picture in galleryManagerCtrl.pictures track by $index'></div>",
restrict:"A",
controller:controllerFn,
controllerAs:"galleryManagerCtrl",
bindToController:{
pictures:"=galleryManager",
}
}
})
.directive("galleryItem", function(FileService){
var linkFn = function($scope, $element, $attrs, galleryManagerCtrl){
$scope.galleryItemCtrl.galleryManagerCtrl = galleryManagerCtrl;
}
var controllerFn = function($scope, $element, $attrs){
var self = this;
if ( self.item instanceof File ){
FileService.buildBlobUrl(self.item).then(function(blobUrl){
self.thumb = blobUrl;
})
}
}
return{
template:"<img src = '{{galleryItemCtrl.thumb}}'>"+
"<a class = 'delete' ng-click = 'galleryItemCtrl.galleryManagerCtrl.removeItemAt($index)'>&times</span></a>",
restrict:"A",
require:"^galleryManager",
link:linkFn,
controller:controllerFn,
bindToController:{
item:"=galleryItem",
},
controllerAs:"galleryItemCtrl"
}
})
Right now, the directive is working well when adding elements, but problems come when removing items; before using: $compile($element)($scope) after the deletion, in the gallery, always dissapeared the last item, although the pictures array removed the correct item, so I added the $compile line after deleting an item.
The problem is that, although the gallery now does what I want to, it keeps throwing an error after compiling (post the full trace, maybe it can help someone):
angular.js:13920 TypeError: Cannot read property 'insertBefore' of null
at after (http://localhost/www/project/admin/bower_components/angular/angular.js:3644:13)
at JQLite.(anonymous function) [as after] (http://localhost/www/project/admin/bower_components/angular/angular.js:3728:17)
at domInsert (http://localhost/www/project/admin/bower_components/angular/angular.js:5282:35)
at Object.move (http://localhost/www/project/admin/bower_components/angular/angular.js:5488:9)
at ngRepeatAction (http://localhost/www/project/admin/bower_components/angular/angular.js:29865:26)
at $watchCollectionAction (http://localhost/www/project/admin/bower_components/angular/angular.js:17385:13)
at Scope.$digest (http://localhost/www/project/admin/bower_components/angular/angular.js:17524:23)
at ChildScope.$apply (http://localhost/www/project/admin/bower_components/angular/angular.js:17790:24)
at HTMLAnchorElement.<anonymous> (http://localhost/www/project/admin/bower_components/angular/angular.js:25890:23)
at defaultHandlerWrapper (http://localhost/www/project/admin/bower_components/angular/angular.js:3497:11)
That seems to come from watchCollection at ngRepeatDirective.
I have the feeling that I´m missing something basic, but can´t see what is right now, so, here I come to ask before digging into angular code.
Thank you in advance.
EDIT
Added a working sample:
http://codepen.io/sergio0983/pen/rMEMoJ?editors=1010
EDIT 2
Removed $compile from working sample, it makes it work, yes, but throws errors; and besides, I think the real problem is elsewhere. In the working sample, you can see how file names get updated when you delete an item, but the pictures keep their original order.
add addPicture() function to your mainController (which will add constant unique ID ro the picture object):
.controller("mainController", function($scope){
$scope.article = {pictures:[]};
$scope.addPicture = function addPicture (picture) {
// set unique ID
picture.id = $scope.article.pictures.length;
$scope.article.pictures.push(picture);
};
})
change add button HTML to:
<div class='md-button l3' ng-click='addPicture(picture)'>Add</div>
change template of galleryManager to track by picture.id:
<div gallery-item='picture'
ng-repeat='picture in galleryManagerCtrl.pictures track by picture.id'></div>
modify removeItemAt() function, ($compile not needed here):
self.removeItemAt = function removeItemAt (id) {
// find index for the picture with given id
id = self.pictures.findIndex((item) => item.id === id);
self.pictures.splice(id, 1);
}
modified codepen: http://codepen.io/anon/pen/mrZOre?editors=1010

Initializing an Angular Directive in JavaScript

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.

AngularJS : update bindings ng-bind-html

I've go a problem with binding. I use the "ng-bind-html" directive, because the details text of the object to display contains HTML code. Unfortunatly the text doesn't update when the object changes. 'title' and 'kurztext' change its values but 'langtext' (the html bound) not. Here's the code I use:
<div class="content scrollContainer" ng-model="selectedItem">
<h2 class="header">{{selectedItem.titel}}</h2>
<div class="kurztext" ng-show="selectedItem.kurztext">{{selectedItem.kurztext}}</div>
<div class="langtext" ng-bind-html="selectedItem.langtext"></div>
</div>
In Javascript I just pick an object out of an array and assign it to $scope.selectedItem to change the displayed item. Here's my JS code (should not be relevant to the problem):
var items;
var app = angular.module("app", ["ngSanitize"]);
app.controller("MainCtrl", function ($scope, $sce){
$scope.items = items;
$scope.selectedItem = $scope.items[0];
$scope.showItem = function(item){ // called on click on list item
$scope.selectedItem = item;
}
$scope.openItemLink = function(id){
for (var i=0; i<$scope.items.length; i++){
if ($scope.items[i].uid==id){
$scope.showItem($scope.items[i]);
break;
}
}
}
$scope.getLink = function(id){
var it = 0;
for (var i=0; i<$scope.items.length; i++){
if ($scope.items[i].uid==id){
it = $scope.items[i];
return it.titel;
}
}
return "----";
}
});
Any ideas?
you need to sanitize it, add angular-sanitize.js and in your module
var App = angular.module('process', ['ngSanitize']);
in your controller
App.controller(
'Init',
[
'$scope', '$sce',
function($scope, $sce)
and your code
$scope.details = $sce.parseAsHtml($scope.details);
i know that if i don't do that it doesnt work too,
according to the documentation
By default, the innerHTML-ed content will be sanitized using the $sanitize service.
To utilize this functionality, ensure that $sanitize is available,
for example, by including ngSanitize in your module's dependencies (not in core Angular.)
You may also bypass sanitization for values you know are safe.
To do so, bind to an explicitly trusted value via $sce.trustAsHtml.
See the example under Strict Contextual Escaping (SCE).

Scope values to a requested content

I have a view that contains a button, when the button is clicked, a $http.get request is executed and the content is appended on the view.
View:
<button ng-click="includeContent()">Include</button>
<div id="container"></div>
Controller:
$scope.includeContent = function() {
$http.get('url').success(function(data) {
document.getElementById('container').innerHTML = data;
}
}
The content to include:
<h1>Hey, I would like to be {{ object }}</h1>
How can I scope a value to object? Do I need to approach this in a complete different way?
The built-in directive ng-bind-html is the way you are looking for.
Beware, that ng-bind-html requires a sanitized string, which is either done automatically when the correct libary is found or it can be done manually ($sce.trustAsHtml).
Don't forget to inject $sce in your controller.
$scope.includeContent = function() {
$http.get('url').success(function(data) {
$scope.data = $sce.trustAsHtml(data);
}
}
<button ng-click="includeContent()">Include</button>
<div ng-bind-html="data"></div>
As you also want to interpolate your requested HTML, I suggest using $interpolate or, if it can contain whole directives or should have a full fledged two-way-data-binding, use $compile instead.
In your case alter the assignment to
$scope.data = $sce.trustAsHtml($interpolate(data)($scope));
Don't forget to inject $interpolate/$compile aswell.
As I don't know about your $scope structure I assume that "object" is available in this scope. If this isn't the case then change the $scope parameter to whatever object contains your interpolation data.
You should use a controller to do this (I imagine you are since you're using $scope).
ctrl function () {
var ctrl = this;
ctrl.includeContent = function () {
$http.get("url").success(function (data) {
ctrl.object = data;
});
};
}
<div ng-controller="ctrl as ctrl">
<button ng-click="ctrl.includeContent()">Include</button>
<div id="container">
<h1 ng-show="ctrl.object">Hey, I would like to be {{ctrl.object}}</h1>
</div>
</div>
You need not select an element and append the data to it. Angular does it for you. That's what is magic about angular.
In your controller's scope, just update object and angular does the heavy-lifting
$scope.includeContent = function() {
$http.get('url').success(function(data) {
$scope.object = data;
}
}
If that's html code from a server, then you should use the 'ng-bind-html' attribute:
<button ng-click="includeContent()">Include</button>
<div id="container" ng-bind-html="htmlModel.ajaxData"></div>
Controller:
$scope.htmlModel = {ajaxData:''};
$scope.includeContent = function() {
$http.get('url').success(function(data) {
$scope.htmlModel.ajaxDataL = data;
}
}
One way is to use ng-bind-html as suggested.
Another way is with $compile:
app.controller('MainCtrl', function($scope, $http, $compile) {
$scope.error='error!!!';
$scope.includeContent = function() {
$http.get('url').success(function(data) {
var elm = angular.element(document.getElementById('container')).html(data);
$compile(elm)($scope);
}).error(function(){
var elm = angular.element(document.getElementById('container')).html('{{error}}');
$compile(elm)($scope);
})
}
});
Also, typically in angular, when you want to manipulate the DOM you use directives.
DEMO

How do I use an Angular directive to show a dialog?

Using Angular, I'm trying to create a directive that will be placed on a button that will launch a search dialog. There are multiple instances of the search button, but obviously I only want a single instance of the dialog. The dialog should be built from a template URL and have it's own controller, but when the user selects an item, the directive will be used to set the value.
Any ideas on how to create the dialog with it's own controller from the directive?
Here's what I've go so far (basically just the directive)...
http://plnkr.co/edit/W9CHO7pfIo9d7KDe3wH6?p=preview
Here is the html from the above plkr...
Find
Here is the code from the above plkr...
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
var person = {};
person.name = 'World';
$scope.person = person;
$scope.setPerson = function(newPerson) {
person = newPerson;
$scope.person = person;
}
});
app.directive('myFind', function () {
var $dlg; // holds the reference to the dialog. Only 1 per app.
return {
restrict: 'A',
link: function (scope, el, attrs) {
if (!$dlg) {
//todo: create the dialog from a template.
$dlg = true;
}
el.bind('click', function () {
//todo: use the dialog box to search.
// This is just test data to show what I'm trying to accomplish.
alert('Find Person');
var foundPerson = {};
foundPerson.name = 'Brian';
scope.$apply(function () {
scope[attrs.myFind](foundPerson);
});
});
}
}
})
This is as far as I've gotten. I can't quite figure out how to create the dialog using a template inside the directive so it only occurs once and then assign it a controller. I think I can assign the controller inside the template, but first I need to figure out how to load the template and call our custom jQuery plugin to generate the dialog (we have our own look & feel for dialogs).
So I believe the question is, how do I load a template inside of a directive? However, if there is a different way of thinking about this problem, I would be interested in that as well.
I will show you how to do it using bootstrap-ui. (you can modify it easily, if it does not suit your needs).
Here is a skeleton of the template. You can normally bound to any properties and functions that are on directive's scope:
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
... // e.g. <div class="button" ng-click=cancel()></div>
</div>
<div class="modal-body">
...
</div>
<div class="modal-footer">
...
</div>
</div>
</div>
Here is how to create/declare directive in your module:
.directive("searchDialog", function ($modal) {
return {
controller: SearchDialogCtrl,
scope : {
searchDialog: '=' // here you will set two-way data bind with a property from the parent scope
},
link: function (scope, element, attrs) {
element.on("click", function (event) { // when button is clicked we show the dialog
scope.modalInstance = $modal.open({
templateUrl: 'views/search.dialog.tpl.html',
scope: scope // this will pass the isoleted scope of search-dialog to the angular-ui modal
});
scope.$apply();
});
}
}
});
Then controller may look something like that:
function SearchDialogCtrl(dep1, dep2) {
$scope.cancel = function() {
$scope.modalInstance.close(); // the same instance that was created in element.on('click',...)
}
// you can call it from the template: search.dialog.tpl.html
$scope.someFunction = function () { ... }
// it can bind to it in the search.dialog.tpl.html
$scope.someProperty;
...
// this will be two-way bound with some property from the parent field (look below)
// if you want to perform some action on it just use $scope.$watch
$scope.searchDialog;
}
Then it your mark-up you can just use it like that:
<div class="buttonClass" search-dialog="myFieldFromScope">search</div>
I recommend this plugin:
https://github.com/trees4/ng-modal
Demo here:
https://trees4.github.io/ngModal/demo.html
Create a dialog declaratively; and it works with existing controllers. The content of the dialog can be styled however you like.

Resources