I'm trying to insert ng-click on a tag that will be inserted in DOM after calling a directive. I use $compile($(this)($scope)) after appending the tag to DOM. but it doesn't work.
app.directive('treeDirectiveSysAdminGuide', function ($rootScope, $compile) {
return {
restricts: 'A',
replace: false,
scope: {
options: '='
}
link: function (scope, element, attrs) {
var deleteNode = null;
scope.new_node = 'new Node';
scope.reload_node = function () {
$(element).each(function () {
var id = $(this).closest("li").attr("id");
if (treeDeleted[id] == 1)
$(this).addClass("deleted");
$(this).append("<a><i id = '" + id + "' class='fa fa-info' **ng-click='nodeInfo(" + id + ")'**></i></a>");
if (!$(this).find('#' + id).hasClass('compiled')) {
$compile($(this).find('#' + id))(scope);
$(this).find('#' + id).addClass('compiled');
}
$(this).before(
$("<a class='jstree-anchor'><i class='fa fa-circle'></i></a>")
.on('click', function () {
scope.options.activate_node && scope.options.activate_node(id, true);
}));
});
});
Try this:
$compile($(this))($scope.$parent);
Additional info: Since nodeInfo() is declared in parent scope, you should pass parent scope to $compile.
Related
I have a directive that dynamically adds child custom directives to the DOM based on some input. Everything works fine. But when the input changes and I re-render the DOM with a different set of child custom directives, the old scopes of the child custom directives are not deleted and hence, the event handlers attached to them are still in memory.
I am re-rendering the DOM by just setting element[0].innerHTML = ''.
Is there a way to delete/destroy the scopes of the custom directive? I saw in some articles that scope.$destroy can be called but how to get a reference of the scope of the child custom directive?
const linker = function (scope, element) {
scope.$watch('data', function () {
reRenderToolbar();
}, true);
const reRenderToolbar = function () {
element[0].innerHTML = '';
_.forEach(scope.data, function (item, key) {
const directive = item.CustomDirective;
scope.options = item.options || {};
html = '<' + directive + ' options="options"></' + directive + '>';
element.append(html);
}
});
}
$compile(element.contents())(scope);
};
The issue was that I was not destroying the childscope in the parent as my app is multiscoped. This article helped me http://www.mattzeunert.com/2014/11/03/manually-removing-angular-directives.html
Code:
const linker = function (scope, element) {
scope.$watch('data', function () {
reRenderToolbar();
}, true);
let childScope;
const reRenderToolbar = function () {
if(childScope) {
childScope.$destroy();
}
element[0].innerHTML = '';
_.forEach(scope.data, function (item, key) {
const directive = item.CustomDirective;
scope.options = item.options || {};
html = '<' + directive + ' options="options"></' + directive + '>';
element.append(html);
}
});
}
childScope = scope.$new()
$compile(element.contents())(childScope);
};
on your custom directive handle the destroy event
directive("CustomDirective", function(){
return {
restrict: 'C',
template: '<div >Custom Directive</div>',
link: function(scope, element){
scope.$on("$destroy",function() {
element.remove();
});
}
}
});
I am using modals to display forms that are presented to users. The forms are all components with an onSave and onCancel method
bindings: {
entity: '<',
readOnly: '<',
onSave: '&',
onCancel: '&'
},
I want to pass into a modal the tag of the form to present in the modal and then pass the parameters returned by the component's onSave/onCancel event back to the modal which will return it to the caller. To do this, I am putting an directive that sets the properties of the component and then runs it through the $compile method to generate it:
function link(scope, elem, attrs) {
if (scope.formType != null && scope.formType != '') {
var objString = "<" + scope.formType + " "; //create beginning tag
objString += "entity='assignedEntity' read-only='readOnly' ";
objString += "on-save='onSave(entity)' on-cancel='onCancel(entity)'>";
objString += "</" + scope.formType + ">"; //add end tag
var obj = $compile(objString)(scope);
elem.append(obj);
}
};
return {
restrict: 'E',
scope: {
formType: '=',
onSave: '&',
onCancel: '&',
assignedEntity: '<',
readOnly: '<'
},
link: link
}
I then call the directive and pass the appropriate properties from a generic modal box like so:
<form-generator
form-type="vm.ui.properties.formType"
on-save="vm.ok(entity)"
on-cancel="vm.cancel(entity)"
assigned-entity="vm.returnItem.entity"
read-only="vm.ui.properties.readOnly">
</form-generator>
This successfully generates the specified form component and passes in the right values for each property down to the form component. My issue is that when the onSave or onCancel events are thrown by the component, the modal controller is receiving the event (vm.ok or vm.cancel gets called) but the parameters passed to those events are not passed up with the call. the properties passed to vm.ok and vm.cancel are always undefined.
From the component, I am calling the onSave/onCancel method like this:
ctrl.onSave({
entity: ctrl.entity
});
and I have verified that ctrl.entity does in fact have a value in it.
Any thoughts as to why the parameters passed back up the call tree are undefined by the time it gets to the modal controller?
I created this plunkr to help define the problem I am having: Example
Please review the code, after a bit of debugging it seems like you just forgot to attach the entity as a part of the function that listens for click $event. Here's the working plunker.
(function() {
var directiveID = "formGenerator";
angular.module('app').directive(directiveID, ['$compile', FormGenerator]);
function FormGenerator($compile) {
function link(scope, elem, attrs) {
console.log(scope, elem, attrs);
if (scope.formType !== null && scope.formType !== '') {
var objString = "<" + scope.formType + " "; //create beginning tag
//PLEASE TAKE A LOOK HERE, WE'RE EXPECTING THE EVENT TO PROPOGATE TO THE PARENT CONTROLLER
//so we take into account the event on-save, the same would have to be done for on-cancel
objString += "on-save='onFormSave($event)' on-cancel='onFormCancel(entity)'>";
objString += "</" + scope.formType + ">"; //add end tag
var obj = $compile(objString)(scope);
elem.append(obj);
}
}
return {
restrict: 'E',
scope: {
formType: '=',
onFormSave: '&',
onFormCancel: '&'
},
link: link
}
}
})();
(function() {
var componentID = "testForm";
var app = angular.module("app");
function TestFormController() {
var ctrl = this;
ctrl.entity = {
name: "this is the entity passed up"
};
ctrl.save = function(event) {
console.log(event);
console.log("In component: " + ctrl.entity.name);
ctrl.onSave({
//AND ON SAVE, we make the this controllers model-properties which you'd like to pass on a part of the event.
$event: {
entity: ctrl.entity
}
});
};
ctrl.cancel = function() {
ctrl.onCancel({
entity: ctrl.entity
});
};
}
app.component(componentID, {
bindings: {
onSave: '&',
onCancel: '&'
},
//Here also we pass the $event to function
template: '<h1> This is a test</h1><button type="button" ng-click="$ctrl.save($event);">Save</button>',
controller: TestFormController
})
}());
I have a main directive that has an array on it's scope that contains data for constructing other directives that should be compiled and appended to the main directive.
The problem is that when I iterate through that array I only get the data from the last element in array,
so I can't properly bind respective data for each custom directive.
Plunker
main directive :
angular.module('testApp')
.directive('mainDirective', ["$compile", function ($compile) {
return {
template: ' <div><p>Main Directive </p><div class="insertion-point"></div></div>',
link: function (scope, element, attributes, controller) {
var insertionPoint = element.find('.insertion-point');
angular.forEach(scope.demoObj.panels, function (value, index) {
var directiveName = value.type;
scope.value = value;
var directiveString = "<div " + directiveName + " panel-data=value ></div>";
var compiledElement = $compile(directiveString)(scope);
insertionPoint.append(compiledElement);
});
}
}
}]);
directive to be nested:
angular.module('testApp')
.directive('nestedDirective', [function () {
return {
scope:{
panelData:'='
},
template:' <div><p>Nested Directive </p>{{panelData.data.test}}</div>'
}
}]);
data looks like this:
$scope.demoObj = {
panels:[
{
id:'unique_id_1',
type:'nested-directive',
data:{
test:'test 1'
}
},
{
id:'unique_id_2',
type:'nested-directive',
data:{
test:'test 2'
}
},
{
id:'unique_id_3',
type:'nested-directive',
data:{
test:'test 3'
}
}
]
}
As far as I can understand , the compilation is not happening immediately in the forEach statement, that's why every directive gets the data from the object with the id unique_id_3 (last element in array). Also all directives have isolated scope.
edit: I understand that in forEach I need to add the value to the scope so I can pass it to the nested directive isolated scope, and I understand that when the loop finishes scope.value will be the last value of the loop, but I was under the impression that compile will immediately pass the value to the nested directive and be done with it.
So, when does the compilation happen?
How can I circumvent this limitation?
The problem is the link step of the compiledElement will happen in the next digest cycle, at that time, scope.value is the last value of the data.
The solution is to create different value properties on scope, like this:
var directiveName = value.type;
var valueProp = 'value' + index;
scope[valueProp] = value;
var directiveString = "<div " + directiveName + " panel-data=" + valueProp + "></div>";
plunk
Please find the update code below. Rather than creating duplicate variable in scope Below is the solution. I have created plunker for the same
angular.module('testApp')
.directive('mainDirective', ["$compile", function ($compile) {
return {
template: ' <div><p>Main Directive </p><div class="insertion-point"></div></div>',
link: function (scope, element, attributes, controller) {
var insertionPoint = element.find('.insertion-point');
angular.forEach(scope.demoObj.panels, function (value, index) {
var directiveName = value.type;
var directiveString = "<div " + directiveName + " panel-data=demoObj.panels["+ index+"]></div>";
var compiledElement = $compile(directiveString)(scope);
insertionPoint.append(compiledElement);
});
}
}
}]);
How can i be notified when a directive is resized?
i have tried
element[0].onresize = function() {
console.log(element[0].offsetWidth + " " + element[0].offsetHeight);
}
but its not calling the function
(function() {
'use strict';
// Define the directive on the module.
// Inject the dependencies.
// Point to the directive definition function.
angular.module('app').directive('nvLayout', ['$window', '$compile', layoutDirective]);
function layoutDirective($window, $compile) {
// Usage:
//
// Creates:
//
var directive = {
link: link,
restrict: 'EA',
scope: {
layoutEntries: "=",
selected: "&onSelected"
},
template: "<div></div>",
controller: controller
};
return directive;
function link(scope, element, attrs) {
var elementCol = [];
var onSelectedHandler = scope.selected();
element.on("resize", function () {
console.log("resized.");
});
$(window).on("resize",scope.sizeNotifier);
scope.$on("$destroy", function () {
$(window).off("resize", $scope.sizeNotifier);
});
scope.sizeNotifier = function() {
alert("windows is being resized...");
};
scope.onselected = function(id) {
onSelectedHandler(id);
};
scope.$watch(function () {
return scope.layoutEntries.length;
},
function (value) {
//layout was changed
activateLayout(scope.layoutEntries);
});
function activateLayout(layoutEntries) {
for (var i = 0; i < layoutEntries.length; i++) {
if (elementCol[layoutEntries[i].id]) {
continue;
}
var div = "<nv-single-layout-entry id=slot" + layoutEntries[i].id + " on-selected='onselected' style=\"position:absolute;";
div = div + "top:" + layoutEntries[i].position.top + "%;";
div = div + "left:" + layoutEntries[i].position.left + "%;";
div = div + "height:" + layoutEntries[i].size.height + "%;";
div = div + "width:" + layoutEntries[i].size.width + "%;";
div = div + "\"></nv-single-layout-entry>";
var el = $compile(div)(scope);
element.append(el);
elementCol[layoutEntries[i].id] = 1;
}
};
}
function controller($scope, $element) {
}
}
})();
Use scope.$watch with a custom watch function:
scope.$watch(
function () {
return [element[0].offsetWidth, element[0].offsetHeight].join('x');
},
function (value) {
console.log('directive got resized:', value.split('x'));
}
)
You would typically want to watch the element's offsetWidth and offsetHeight properties. With more recent versions of AngularJS, you can use $scope.$watchGroup in your link function:
app.directive('myDirective', [function() {
function link($scope, element) {
var container = element[0];
$scope.$watchGroup([
function() { return container.offsetWidth; },
function() { return container.offsetHeight; }
], function(values) {
// Handle resize event ...
});
}
// Return directive definition ...
}]);
However, you may find that updates are quite slow when watching the element properties directly in this manner.
To make your directive more responsive, you could moderate the refresh rate by using $interval. Here's an example of a reusable service for watching element sizes at a configurable millisecond rate:
app.factory('sizeWatcher', ['$interval', function($interval) {
return function (element, rate) {
var self = this;
(self.update = function() { self.dimensions = [element.offsetWidth, element.offsetHeight]; })();
self.monitor = $interval(self.update, rate);
self.group = [function() { return self.dimensions[0]; }, function() { return self.dimensions[1]; }];
self.cancel = function() { $interval.cancel(self.monitor); };
};
}]);
A directive using such a service would look something like this:
app.directive('myDirective', ['sizeWatcher', function(sizeWatcher) {
function link($scope, element) {
var container = element[0],
watcher = new sizeWatcher(container, 200);
$scope.$watchGroup(watcher.group, function(values) {
// Handle resize event ...
});
$scope.$on('$destroy', watcher.cancel);
}
// Return directive definition ...
}]);
Note the call to watcher.cancel() in the $scope.$destroy event handler; this ensures that the $interval instance is destroyed when no longer required.
A JSFiddle example can be found here.
Here a sample code of what you need to do:
APP.directive('nvLayout', function ($window) {
return {
template: "<div></div>",
restrict: 'EA',
link: function postLink(scope, element, attrs) {
scope.onResizeFunction = function() {
scope.windowHeight = $window.innerHeight;
scope.windowWidth = $window.innerWidth;
console.log(scope.windowHeight+"-"+scope.windowWidth)
};
// Call to the function when the page is first loaded
scope.onResizeFunction();
angular.element($window).bind('resize', function() {
scope.onResizeFunction();
scope.$apply();
});
}
};
});
The only way you would be able to detect size/position changes on an element using $watch is if you constantly updated your scope using something like $interval or $timeout. While possible, it can become an expensive operation, and really slow your app down.
One way you could detect a change on an element is by calling
requestAnimationFrame.
var previousPosition = element[0].getBoundingClientRect();
onFrame();
function onFrame() {
var currentPosition = element[0].getBoundingClientRect();
if (!angular.equals(previousPosition, currentPosition)) {
resiszeNotifier();
}
previousPosition = currentPosition;
requestAnimationFrame(onFrame);
}
function resiszeNotifier() {
// Notify...
}
Here's a Plunk demonstrating this. As long as you're moving the box around, it will stay red.
http://plnkr.co/edit/qiMJaeipE9DgFsYd0sfr?p=preview
A slight variation on Eliel's answer worked for me. In the directive.js:
$scope.onResizeFunction = function() {
};
// Call to the function when the page is first loaded
$scope.onResizeFunction();
angular.element($(window)).bind('resize', function() {
$scope.onResizeFunction();
$scope.$apply();
});
I call
$(window).resize();
from within my app.js. The directive's d3 chart now resizes to fill the container.
Here is my take on this directive (using Webpack as bundler):
module.exports = (ngModule) ->
ngModule.directive 'onResize', ['Callback', (Callback) ->
restrict: 'A'
scope:
onResize: '#'
onResizeDebounce: '#'
link: (scope, element) ->
container = element[0]
eventName = scope.onResize || 'onResize'
delay = scope.onResizeDebounce || 1000
scope.$watchGroup [
-> container.offsetWidth ,
-> container.offsetHeight
], _.debounce (values) ->
Callback.event(eventName, values)
, delay
]
I use angularjs with "ui_select2" directive. Select2 draws new tags with formatting function, there are "" elements with "ng-click" attribute. How to tell angularjs about new DOM elements? Otherwise new "ng-clicks" wont work.
HTML:
<input type="text" name="contact_ids" ui-select2="unit.participantsOptions" ng-model="unit.contactIds" />
JS (angular controller):
anyFunction = function(id) {
console.log(id)
}
formatContactSelection = function(state) {
return "<a class=\"select2-search-choice-favorite\" tabindex=\"-1\" href=\"\" ng-click=\"anyFunction(state.id)\"></a>"
}
return $scope.unit.participantsOptions = {
tags: [],
formatSelection: formatContactSelection,
escapeMarkup: function(m) {
return m
},
ajax: {
url: '/contacts/search',
quietMillis: 100,
data: function(term, page) {
return {
term: term,
limit: 20,
page: page
}
},
results: function(data, page) {
return {
results: data,
more: (page * 10) < data.total
}
}
}
}
The problem is that select2 creates DOM elements, that not yet discovered by angularjs, I read that new DOM elements need to be appended to some element with using angularjs $compile function, but I cannot use it in controller.
I found a solution - I created the directive, that watches for changes in ngModel and apply it on the element, that has already ui_select2 directive. "uiRequiredTags" implements custom behavior I need for my select2 tag choices. The solution is to watch changes in ngModel attribute.
angular.module("myAppModule", []).directive("uiRequiredTags", function() {
return {
restrict: 'A',
require: "ngModel",
link: function(scope, el, attrs) {
var opts;
opts = scope.$eval("{" + attrs.uiRequiredTags + "}");
return scope.$watch(attrs.ngModel, function(val) {
var $requireLink;
$requireLink = el.parent().find(opts.path);
$requireLink.off('click');
$requireLink.on('click', function() {
var id, n, tagIds;
id = "" + ($(this).data('requiredTagId'));
if (opts.removeLinkPath && opts.innerContainer) {
$(this).parents(opts.innerContainer).find(opts.removeLinkPath).data('requiredTagId', id);
}
tagIds = scope.$eval(opts.model).split(',');
n = tagIds.indexOf(id);
if (n < 0) {
tagIds.push(id);
} else {
tagIds.splice(n, 1);
}
scope.$eval("" + opts.model + " = '" + tagIds + "'");
scope.$apply();
return $(this).toggleClass('active');
});
});
}
};