Call a function in a angular-controller from outside of the controller? - angularjs

I have a lightbox-dierective and controller that looks like this:
directive('modalDialog', function() {
return {
restrict: 'E',
scope: {
show: '='
},
replace: true, // Replace with the template below
transclude: true, // we want to insert custom content inside the directive
template: '<div class="ng-modal" ng-show="show"><div class="ng-modal-overlay" ng-click="hideModal()"></div><div class="ng-modal-dialog" ng-style="dialogStyle"><div class="ng-modal-dialog-content" ng-transclude><div class="ng-modal-close" ng-click="hideModal()">X</div></div></div></div>'
};
}).controller('Lightbox', function($scope) {
$scope.modalShown = false;
$scope.toggleModal = function() {
$scope.modalShown = !$scope.modalShown;
};
});
Here is the desierd html, what I need is to open the secon ligthbox from withing the first one:
<div ng-controller="Lightbox">
<span ng-mousedown='toggleModal()'>Open lightbox one</span>
<modal-dialog show='modalShown'>
<h2>One lightbox <span ng-mousedown='toggleModal()'>Open lightbox two</span></h2>
</modal-dialog>
</div>
<div ng-controller="Lightbox">
<span ng-mousedown='toggleModal()'>Open lightbox one</span>
<modal-dialog show='modalShown'>
<h2>another lightbox</h2>
</modal-dialog>
</div>
For most cases it works great! I use it in several occations throughout the site, with diffrent lightboxes and different content.
I have now come across a case, where I need to call one of the lightboxes from outside of the controller. Can this be achieved and in that case how do I reference the right lightbox?

I'd extend that setting to an object
var modalSet = {
shown: false,
toggle: function(){ modalSet.shown = !modalSet.shown }
}
Then put it on your main controller (the one with ngApp attribute) and have your entire scope modaleble.
Also, directives do have a controller option, but since only one modal is gonna show up at any given time, you might not want to re-create a controller for every new instance.
Upon re-reading your question: Where is it exactly -> "outside of the controller"?

Related

How do I specify the parent I want to transclude a directive to?

I have a lightbox directive that when it transcludes, it injects in the content. Technically, wherever I put the actual call to the directive. But essentially, I need it to transclude to the body so that the backdrop can cover the entire page viewport, and so that it centers to the viewport and not the container is transcluding to right now.
The directive is written this way:
(function() {
'use strict';
angular
.module('app.core')
.directive('lightboxDirective', lightboxDirective);
function lightboxDirective() {
return {
restrict: 'E',
transclude: true,
scope: {},
template: '<section class="md-lightbox" ng-transclude></section>',
};
}
})();
The lightbox is located in the page this way:
<div id="scala-media-media" class="page-layout">
<div class="center" flex>
<div class="header">
<img ng-src="{{vm.media.images[0].url}}" ng-click="showLightBox = true">
</div>
<div class="content">
... the page content here ...
</div>
</div>
</div>
<lightbox-directive class="angular-lightbox" ng-class="{ removed : !showLightBox }">
... the code for my lightbox content (which works fine by the way) ...
</lightbox-directive>
At first I thought I'd specify the parent as I would normally do in a $mdDialog (Angular Material) with parent: angular.element($document.body), but that didn't work out. parent is not a recognized callback I assume.
Notice in the image where it is injected. Right where I place it! What's the point of using a directive if I can just place the code there and will do the same thing without the directive? right?
Here is a screenshot of what is happening. Notice the backdrop and centering issue I am having on the left side, as opposed to what I desire in the right side with a dialog.
I am using this angular lightbox found in this CODEPEN
UPDATE
I moved the lightbox to its own template file, so as to not feel guilty for using the code directly on the page which makes the use of a directive redundant. And restructured my directive as it shows below. But the rendering on runtime is still a problem. The template injects where it is called. Now if only I could declare the body as the parent for it to append to! lol
New directive:
function lightboxDirective() {
return {
restrict: 'E',
templateUrl: function(elem,attrs) {
return attrs.templateUrl || 'app/main/pages/scala-media/views/lightbox-template.html'
}
};
}
Thanks in advance!
You can make the element reposition itself just before the closing body tag with
.directive("lightboxDirective", function() {
return {
restrict: 'E',
transclude: true,
scope: {},
template: '<section class="md-lightbox" ng-transclude></section>',
link: function(scope, elem) {
angular.element(document).find("body").append(elem);
}
};
});

Creating child scopes in AngularJS

I have a savingIndicator directive that will show a spinny, and then success / error messages. It binds to things like saving and message to show information to the user. The directive is pretty dumb and literally just loads a template like so
m.directive('savingIndicator', function () {
return {
restrict: 'E',
templateUrl: '/templates/savingindicator'
};
});
SavingIndicator.html (snippet):
<span ng-show="saving">
<img src="/Content/loader.gif" style="vertical-align: middle;" /> Saving...
</span>
In my current controller I have a number of things that need to save, so I want to separate them like so:
var thingToSave1 = {
saving: false,
message: ""
}
var thingToSave2 = {
saving: false,
message: ""
}
How can I tell savingIndicator not to look on the main scope (the controller) for its variables, but inside one of these objects? In my head it would work something like what is below, but its not currently working
<input text="Save item 1" />
<saving-indicator ng-model="thingToSave1"></saving-indicator>
...
<input text="Save item 2" />
<saving-indicator ng-model="thingToSave2"></saving-indicator>
You need to add another parameter in the directive's definition. The parameter you need is called "scope". Check out Angular's documentation is already explained there. If you want to play a little bit, this is a plunker (from angular docs):
...
return {
restrict: 'E',
scope: {
customerInfo: '=info'
},
templateUrl: 'my-customer-iso.html'
};
http://plnkr.co/edit/XFy6IB0cdBTMglItIgWR.
In that way you are specifying a new scope for the directive instead of using the controller one.
A quick hack would be as follows:
m.directive('savingIndicator', function () {
return {
restrict: 'E',
templateUrl: '/templates/savingindicator',
scope: {
myModel: '='
}
};
});
//---------------
<span ng-show="myModel.saving">
<img src="/Content/loader.gif" style="vertical-align: middle;" />Saving...
</span>
//---------------
<saving-indicator my-model="thingToSave1"></saving-indicator>
You can learn more about directives and isolate scope option in Angular docs.

Why does my nested directive disconnect from its ngModel as soon as $setViewValue() is called?

Plunker here.
I have a directive ("child") nested inside another directive ("parent"). It requires ngModel, and ngModelCtrl.$modelValue is shown and kept up-to-date just fine in its template. That is, until I call ngModelCtrl.$setViewValue().
So here is the HTML initialising the directives:
<div parent>
<div child ng-model="content">Some</div>
</div>
And here are the directives:
angular.module('form-example2', [])
.controller('MainCtrl', function($scope){
$scope.content = 'Hi';
})
.directive('parent', function() {
return {
transclude: true,
template: '<div ng-transclude></div>',
controller: function(){
},
scope: {}
};
})
.directive('child', function() {
return {
require: ['ngModel', '^parent'],
transclude: true,
template: '<div>Model: {{model.$modelValue}} (<a style="text-decoration: underline; cursor: pointer;" ng-click="alter()">Alter</a>)<br />Contents: <div style="background: grey" ng-transclude></div></div>',
scope: {},
link: function(scope, elm, attrs, ctrl) {
var ngModelCtrl = ctrl[0];
var parentCtrl = ctrl[1];
scope.model = ngModelCtrl;
// view -> model
scope.alter = function(){
ngModelCtrl.$setViewValue('Hi2');
}
// model -> view
// load init value from DOM
}
};
});
When the model (i.e. content) changes, this change can be seen inside the child directive. When you click the "Alter" link (which triggers a call of $setViewValue()), the model's value should become "Hi2". This is correctly displayed inside the child directive, but not in the model outside the directive. Furthermore, when I now update the model outside the directive, it is no longer updated inside the directive.
How come?
The directives ended up being just fine; the only problem was that the passed model should be an object property. Hence, the directives work if the following modifications are made to the calling code (Plunker):
In the controller, instead of $scope.content = 'Hi';:
$scope.content = {
value: 'Hi'
};
In the template, replace all references to content with content.value:
<input ng-model="content.value" type="text" />
<div parent>
<div child ng-model="content.value">Some</div>
</div>
<pre>model = {{content.value}}</pre>
The reason this works, roughly, is that when Angular passes the reference to the model to the transcluded scope of the parent directive (i.e. the one the child is in), this is only a reference when it refers to an object property - otherwise it is a copy, which Angular cannot watch for changes.
#Josep's answer helped greatly so, even though it did not provide the actual solution, if you're reading this and it's useful, give it a vote :)

Detect if a transclude content has been given for a angularjs directive

I have a directive (a progressbar) which should have two possible states, one without any description and one with a label on the left side.
It would be cool to simple use the transcluded content for this label.
Does anyone know how I can add a class to my directive depending whether a transclude content has been given or not?
So I want to add:
<div class="progress" ng-class="{withLabel: *CODE GOES HERE*}">
<div class="label"><span ng-transclude></span>
<div class="other">...</div>
</div>
Thanks a lot!
After release of Angular v1.5 with multi-slot transclusion it's even simpler. For example you have used component instead of directive and don't have access to link or compile functions. Yet you have access to $transclude service. So you can check presence of content with 'official' method:
app.component('myTransclude', {
transclude: {
'slot': '?transcludeSlot'
},
controller: function ($transclude) {
this.transcludePresent = function() {
return $transclude.isSlotFilled('slot');
};
}
})
with template like this:
<div class="progress" ng-class="{'with-label': withLabel}">
<div class="label"><span ng-transclude="slot"></span>
<div class="other">...</div>
</div>
Based on #Ilan's solution, you can use this simple $transclude function to know if there is transcluded content or not.
$transclude(function(clone){
if(clone.length){
scope.hasTranscluded = true;
}
});
Plnkr demonstrating this approach with ng-if to set default content if nothing to transclude: http://plnkr.co/hHr0aoSktqZYKoiFMzE6
Here is a plunker: http://plnkr.co/edit/ednJwiceWD5vS0orewKW?p=preview
You can find the transcluded element inside the linking function and check it's contents:
Directive:
app.directive('progressbar', function(){
return {
scope: {},
transclude: true,
templateUrl: "progressbar.html",
link: function(scope,elm){
var transcluded = elm.find('span').contents();
scope.withLabel = transcluded.length > 0; // true or false
}
}
})
Template:
<div class="progress" ng-class="{'with-label': withLabel}">
<div class="label"><span ng-transclude></span>
<div class="other">...</div>
</div>
You can also create your custom transclusion directive like so:
app.directive('myTransclude', function(){
return {
link: function(scope, elm, attrs, ctrl, $transclude){
$transclude(function(clone){
// Do something with this:
// if(clone.length > 0) ...
elm.empty();
elm.append(clone);
})
}
}
})
Based on the solution from #plong0 & #Ilan, this seems to work a bit better since it works with whitespace as well.
$transcludeFn(function(clonedElement) {
scope.hasTranscludedContent = clonedElement.html().trim() === "";
});
where previously <my-directive> </my-directive> would return that it has a .length of 1 since it contains a text node. since the function passed into $transcludeFn returns a jQuery object of the contents of the transcluded content, we can just get the inner text, remove whitespace on the ends, and check to see if it's blank or not.
Note that this only checks for text, so including html elements without text will also be flagged as empty. Like this: <my-directive> <span> </span> </my-directive> - This worked for my needs though.

Changing src of only hovered ng-include element, where value of src is a $scope variable

I have multiple ng-include elements that have src attribute set to $scope.template_url.
I want to change src of hovered element only to new template but changing it's value will change all of elements. How can i implement it?
Html code:
<section class="parent">
<div data-ng-include data-src="template_url"></div>
</section>
Javascript (in controller):
angular.element(document).on('mouseover', '.parent', function(){
$scope.$apply(function () {
$scope.template_url = "path/to/new/template.html";
});
});
Writing jQuery dom manipulation is dirty and also don't works:
$(this).attr('data-src', "path/to/new/template.html");
I'd suggest making this a directive. Directives have their own scope, so you can still do the "on hover use a different template" idea, but for each individual one that is hovered.
<div>
<div data-some-directive=""></div>
</div>
var myApp = angular.module('myApp',[]);
myApp.directive('someDirective', function() {
return {
controller: function ($scope) {
$scope.model = "Hello"
$scope.mouseover = function () {
$scope.model = "Hovered!";
};
},
scope:{},
restrict: 'AE',
replace: true,
template: '<div><input ng-mouseover="mouseover()" ng-model="model"></div>',
};
});
Heres a fiddle to see it in action.
Tweak the template variable in the directive to use a variable on your model for the include url.
By the way, angular already has a mouseover handler, so i've just linked that into the controller with ng-mouseover in the template.

Resources