AngularJS Dynamic compilation of component not passing events up - angularjs

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
})
}());

Related

How to remove a scope of a component injected from a parent directive

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();
});
}
}
});

re-use google-places autocomplete input after page navigation

I need in a angularjs single page application a google-places autocomplete input, that shall run as a service and shall be initialized once at runtime. In case of navigation, the with goolge-places initialized element and the appropriate scope are destroyed.
I will re-use the places input field after navigate to the page containing the places autocomplete input field. With the method element.replaceWith() it works well.
After replacing the element, I can not reset the input by the "reset" button. How can I bind the new generated scope to the "reset" button and the old scope variables. Because the old scope and elements are destroyed by the navigation event?
.factory('myService', function() {
var gPlace;
var s, e;
var options = {
types: [],
componentRestrictions: {country: 'in'}
};
function init() {
}
function set(element, scope) {
console.log('set');
if (!gPlace) {
e = element;
gPlace = new google.maps.places.Autocomplete(element[0], options);
google.maps.event.addListener(gPlace, 'place_changed', function() {
scope.$apply(function() {
scope.place.chosenPlace = element.val();
});
});
} else {
element.replaceWith(e);
}
}
init();
return {
'init':init,
'set':set
};
});
the navigation (element and scope destroying) will be simulated in this plunk by the ng-if directive that will be triggered by the "remove" button.
see here plunk
If you want you can create a service that holds the last selected place and shares it among controllers and directives:
.service('myPlaceService', function(){
var _place;
this.setPlace = function(place){
_place = place;
}
this.getPlace = function(){
return _place;
}
return this;
});
Then create a directive that uses this service:
.directive('googlePlaces', function(myPlaceService) {
return {
restrict: 'E',
scope: {
types: '=',
options: '=',
place: '=',
reset: '='
},
template: '<div>' +
'<input id="gPlaces" type="text"> <button ng-click="resetPlace()">Reset</button>' +
'</div>',
link: function(scope, el, attr){
var input = document.querySelector('#gPlaces');
var jqEl = angular.element(input);
var gPlace = new google.maps.places.Autocomplete(input, scope.options || {});
var listener = google.maps.event.addListener(gPlace, 'place_changed', function() {
var place = autocomplete.getPlace();
scope.$apply(function() {
scope.place.chosenPlace = jqEl.val();
//Whenever place changes, update the service.
//For a more robust solution you could emit an event using scope.$broadcast
//then catch the event where updates are needed.
//Alternatively you can $scope.$watch(myPlaceService.getPlace, function() {...})
myPlaceService.setPlace(jqEl.val());
});
scope.reset = function(){
scope.place.chosenPlace = null;
jqEl.val("");
}
scope.$on('$destroy', function(){
if(listener)
google.maps.event.removeListener(listener);
});
});
}
}
});
Now you can use it like so:
<google-places place="vm.place" options="vm.gPlacesOpts"/>
Where:
vm.gPlacesOpts = {types: [], componentRestrictions: {country: 'in'}}

inserting new ng-click after first compile in angular js

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.

Invoke a Method Directly on a Sibling Directive

Is it possible to call a method directly on a specific directive if the ID of that specific directive is known? I know how to do it through listener events (broadcast or emit). I suppose I could do my manipulation using jQuery but I'd like to be able to do it only through Angular. Also, I'd like to avoid the listener event because it seems "wasteful" for every instance of that directive to have to determine if that particular event "belongs" to them.
HTML
<custom-element ce-Id="5"></custom-element>
<custom-element ce-Id="6"></custom-element>
<custom-element ce-Id="7"></custom-element>
<custom-element ce-Id="8"></custom-element>
<custom-element ce-Id="9"></custom-element>
<custom-element ce-Id="10"></custom-element>
So using the example above, is it possible for an event on directive ce-Id="6" (say a click event) to trigger something to happen specifically on ce-Id="7" without using a listener?
You can define a custom API in the factory function of the directive and keep track of subscribers. This code will only run once. Can move it to a service as well.
Example:
app.directive('customElement', function() {
var subscribers = {};
var subscribe = function(id, callback) {
subscribers[id] = callback;
};
var unsubscribe = function(id) {
subscribers[id] = null;
};
var notify = function(id) {
var target = parseInt(id) + 1;
var action = subscribers[target];
if (action) action();
};
var api = {
subscribe: subscribe,
unsubscribe: unsubscribe,
notify: notify
};
return {
restrict: 'E',
template: '<div>I am custom element: {{ ceId }}</div>',
scope: {
ceId: '#',
},
link: function(scope, element, attrs) {
var id = scope.ceId;
if (!id) return;
var onReceive = function() {
console.log('customElement ' + id + ' has received notification.');
};
api.subscribe(id, onReceive);
var onClick = function() {
scope.$apply(function () {
api.notify(id);
});
};
element.on('click', onClick);
scope.$on('$destroy', function() {
element.off('click', onClick);
api.unsubscribe(id);
});
}
};
});
Demo: http://plnkr.co/edit/2s1bkToSuHPURQUcvZcd?p=preview

Why is this ng-show directive not working in a template?

I am trying to write a directive that will do a simple in-place edit for an element. This is my code so far:
directive('clickEdit', function() {
return {
restrict: 'A',
template: '<span ng-show="inEdit"><input ng-model="editModel"/></span>' +
'<span ng-show="!inEdit" ng-click="edit()">{{ editModel }}</span>',
scope: {
editModel: "=",
inEdit: "#"
},
link: function(scope, element, attr) {
scope.inEdit = false;
var savedValue = scope.editModel;
var input = element.find('input');
input.bind('keyup', function(e) {
if ( e.keyCode === 13 ) {
scope.save();
} else if ( e.keyCode === 27 ) {
scope.cancel();
}
});
scope.edit = function() {
scope.inEdit = true;
setTimeout(function(){
input[0].focus();
input[0].select();
}, 0);
};
scope.save = function() {
scope.inEdit = false;
};
scope.cancel = function() {
scope.inEdit = false;
scope.editModel = savedValue;
};
}
}
})
The scope.edit function sets inEdit to true, and that works well - it hides the text and shows the input tag. However, the scope.save function, which sets scope.inEdit to false does not work at all. It does not hide the input tag and show the text.
Why?
You are calling scope.save() from a event handler reacting to the keyup event. However this event handler is not called by/through the AngularJS framework. AngularJS will only scan for changes of the model if it believes that changes might have occured in order to lessen the workload (AngularJS as of now does dirty-checking with is computational intensive).
Therefore you must make use of the scope.$apply feature to make AngularJS aware that you are doing changes to the scope. Change the scope.save function to this and it shall work:
scope.save = function(){
scope.$apply(function(){
scope.inEdit = false;
});
});
Also it appears that there is actually no need to bind this save function to a scope variable. So you might want to instead define a "normal" function or just integrate the code into your event handler.

Resources