Communication between directive function and transcluded controller - angularjs

I have a directive that "manages" all the instances of the associated component. I would like the directive function to be able to call a function in the transcluded controller, and vice versa. An isolate scope in the link function does not provide this capability. Is this even possible in Angular 1.x? I have everything working, but only by using $broadcast() and hard-coding names into all the child controllers. It feels like a hack.
The directive looks like this:
app.directive('myDirective', function () {
return {
transclude: true,
template: '...<ng-transclude />...',
link: function (scope, elem, attrs) {
...
}
};
});
And the transcluded content looks like this:
<div ng-controller="MyController">
...
</div>
Ideally, I would like to be able to "inject" an object from the directive function into the transcluded controller that contains data and callbacks.

Related

Dynamically update a directive attribute using vanilla JavaScript

I have an Angular JS directive that looks something like this:
function ($sce) {
'use strict';
return {
restrict: 'E',
templateUrl: $sce.trustAsResourceUrl('...'),
scope: {
serviceName: '#?',
},
controller: 'MyController',
link: function () {}
};
}
The directive is instantiated like so:
<my-directive service="My Cool Service"></my-directive>
Recently, some consumers of this directive would like the ability to modify the service attribute after the directive has been instantiated and have the directive reflect the change. Here is an example of what a specific consumer is doing:
const directive = document.querySelector('my-directive');
directive.setAttribute('service', 'Another Service Name');
This makes sense; however, the directive does not reflect the change once they set the attribute. I am figuring out a way to accomplish this. I have tried using scope.$watch and $observe to no avail; example:
link: function (_, _, attrs) {
attrs.$observe("serviceName", newValue => updateServiceName(newValue));
}
Any insights on how to accomplish this? Thanks!
The serviceName property inside your directive's scope definition should be bound with = instead of #. Then, from the outer code, just pass in the service name with a variable.
$scope.myServiceName = "My Cool Service";
<my-directive service="{{myServiceName}}"></my-directive>
Now if you change the myServiceName value, the change will be reflected in the child directive.

How to access controller of a child directive

If I have a directive FilePicker that contains in its template an instance of the Modal directive, how can I get a reference to an instance of the Modal directive's controller from within the FilePicker's controller?
I ask, of course, because I cannot find any way to do this, but this is a bitter disappointment in light of everything I've heard about directive controllers being the cornerstone of directive-to-directive communication rather than doing everything via $scope.
An instance of something in template? What? A template is just a simple string and that's all.
About controllers:
DOM elements can be managed by angular controllers. Directives are being applied to DOM elements. Then can use controllers too. You can simply describe a directive controller by:
// directive with name parentDirective
{
link: function () { ... },
restrict: 'A',
controller: [ '$scope', function ($scope) {
this.sayHello = function () { alert('hello'); }
// 'this' references the instance of the directive controller and then can be required by a child
}],
template: '<div><child-directive/></div>'
}
// child directive with the name childDirective
{
require: '^parentDirective',
link: function (scope, $element, attributes, parentDirectiveController) {
parentDirectiveController.sayHello();
}
}

Angularjs generic directive and DOM manipulation

I have a Directive that I want it to be generic to the application.
And I'd like to know if it is a good practice to do DOM manipulation inside the Controller if not please could you explain where I'am supposed to do it.
Should I create a service like MyDirectiveUIService or something similar to externalize all DOM manipulation..?
Thank you
My app looks like this
In the template I call myDirective and myFunction
<div my-directive my-function="myFunction()"></div>
Then myDirective call myFunction in the link function
myApp.directive( 'myDirective', function() {
function link( scope, element, attributes ) {
scope.myFunction(element)
}
return {
scope:{
myFunction:'&'
},
link:link
}
}
In all the controllers where I use the directive I have the function definition as
$scope.myFunction = function(element){
//DIFFERENT DOM MANIPULATION FOR EACH CONTROLLERS
}

Is it ok watch the scope inside a custom directive?

I'm trying to write a directive to create a map component so I can write something like:
<map></map>
Now the directive looks like this:
angular.module('myApp')
.directive('map', function (GoogleMaps) {
return {
restrict: 'E',
link: function(scope, element, attrs) {
scope.$watch('selectedCenter', function() {
renderMap(scope.selectedCenter.location.latitude, scope.selectedCenter.location.longitude, attrs.zoom?parseInt(attrs.zoom):17);
});
function renderMap(latitude, longitude, zoom){
GoogleMaps.setCenter(latitude, longitude);
GoogleMaps.setZoom(zoom);
GoogleMaps.render(element[0]);
}
}
};
});
The problem is that 'watch' inside the directive doesn't looks very well thinking in the reusability of the component. So I guess the best thing is being able to do something like:
<map ng-model="selectedCenter.location"></map>
But I don't know if it's even a good thing using angular directives inside custom directives or how can I get the object indicated in the ng-model attribute in the custom-directive's link function.
You will need to do something like that
angular.module('myApp')
.directive('map', function (GoogleMaps) {
return {
restrict: 'E',
scope: {
ngModel: '=' // set up bi-directional binding between a local scope property and the parent scope property
},
as of now you could safely watch your scope.ngModel and when ever the relevant value will be changed outside the directive you will be notified.
Please note that adding the scope property to our directive will create a new isolated scope.
You can refer to the angular doc around directive here and especially the section "Directive Definition Object" for more details around the scope property.
Finally you could also use this tutorial where you will find all the material to achieve a directive with two way communication form your app to the directive and opposite.
Without scope declaration in directive:
html
<map ng-model="selectedCenter"></map>
directive
app.directive('map', function() {
return {
restrict: 'E',
require: 'ngModel',
link: function(scope, el, attrs) {
scope.$watch(attrs.ngModel, function(newValue) {
console.log("Changed to " + newValue);
});
}
};
});
One easy way you can achieve this would be to do something like
<map model="selectedCenter"></map>
and inside your directive change the watch to
scope.$watch(attrs.model, function() {
and you are good to go

AngularJS directive transclude part binding

I'd like to use a directive, transclude content, and call directive's controller method within the transcluded part:
<mydirective>
<div ng-click='foo()'>
click me
</div>
</mydirective>
app.directive "mydirective", ->
return {
restrict: 'EACM',
transclude: true
template: "<div ng-transclude></div>"
scope: { } #required: I use two way binding on some variable, but it's not the question here
controller: [ '$scope', ($scope)->
$scope.foo = -> console.log('foo')
]
}
plunkr here.
How can I do that please?
I have a different answer, which is not a hack and I hope it will be accepted..
see my plunkr for a live demo
Here is my usage of the directive
<div custom-directive custom-name="{{name}}">
if transclude works fine you should see my name right here.. [{{customName}}]
</div>
Note I am using customName within the directive and I assign it a value as part of the directive's scope.
Here is my directive definition
angular.module('guy').directive('customDirective', function($compile, $timeout){
return {
template : '<div class="custom-template">This is custom template with [{{customName}}]. below should be appended content with binding to isolated scope using the transclude function.. wait 2 seconds to see that binding works</div>',
restrict: 'AC',
transclude: true,
scope : {
customName : '#'
},
link : function postLink( scope, element, attrs, dummy, transcludeFn ){
transcludeFn( scope, function(clone, innerScope ){
var compiled = $compile(clone)(scope);
element.append(compiled);
});
$timeout( function(){
scope.customName = 'this stuff works!!!';
}, 2000);
}
}
});
Note that I am changing the value on the scope after 2 seconds so it shows the binding works.
After reading a lot online, I understood the following:
the ng-transclude directive is the default implementation to transclusion which can be redefined per use-case by the user
redefining a transclusion means angular will use your definition on each $digest
by default - the transclusion creates a new scope which is not a child of the isolated scope, but rather a sibling (and so the hack works). If you redefine the transclusion process you can choose which scope is used while compiling the transcluded content.. -- even though a new scope is STILL created it seems
There is not enough documentation to the transclude function. I didn't even find it in the documentation. I found it in another SO answer
This is a bit tricky. The transcluded scope is not the child of the directive scope, instead they are siblings. So in order to access foo from the ng-click of the transcluded element, you have to assign foo to the correct scope, i.e. the sibling of the directive scope. Be sure to access the transcluded scope from the link function because it hasn't been created in controller function.
Demo link
var app = angular.module('plunker', []);
app.directive("mydirective", function(){
return {
transclude: true,
restrict: 'EACM',
template: "<div> {{ name }} <br/><br/> <div ng-transclude> </div></div>",
scope: { },
link: function($scope){
$scope.name = 'Should change if click below works';
$scope.$$nextSibling.foo = function(){
console.log('foo');
$scope.name = 'it works!';
}
}
}
})
Another way is assigning foo to the parent scope because both prototypally inherits from the parent scope, i.e.
$scope.$parent.foo = ...
Technically, if you remove scope: { }, then it should work since the directive will not create an isolated scope. (Btw, you need to add restrict: "E", since you use the directive as element)
I think it makes more sense to call actions defined in parent scope from directive rather than call the actions in the directive from parent scope. Directive should be something self-contained and reusable. The actions in the directive should not be accessible from outside.
If you really want to do it, you can try to emit an event by calling $scope.$broadcast(), and add a listener in the directive. Hope it helps.

Resources