Multiple ngIncludes with Different controls visible - angularjs

I have a snippet of HTML with its own controller. I want to include that snippet in two different places.
If it is displayed as a child of #parent1, I want some fields hidden. If displayed as part of #parent2, I'd like other fields hidden.
I've done this before, but not when #parent1 and #parent2 can both be visible at the same time.
Thoughts?

You are at the point where you should probably stop using ng-include and write a very simple directive instead. This is the typical use case of a angular directive with an isolated scope, just pass in a scope-variable and use it in your template with ngShow ngHide or ngIf:
.directive('snippy', ['$rootScope',
function ($rootScope) {
return {
restrict: 'E',
scope: {
showit: '='
},
templateUrl: 'yoursnippet.html',
link: function(scope, elem, attrs) {
// here goes your controller code
}
}
and then in yoursnippet.html:
<div>
<div ng-show="showit">this is shown/hidden</div>
</div>
and then in your parent:
<div>
<snippy showit="anyangularexpression">
<snippy showit="anyangularexpression2">
</div>

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

AngularJS $watch controller variable from a directive with scope

From the directive, I want to track changes to a controller variable using $watch.
I have created this jsfiddle. (https://jsfiddle.net/hqz1seqw/7/)
When the page loads, the controller and both directives $watch function gets called but when I change the radio buttons, only the controllers and dir-two $watch function gets called. Why isnt dir-ones $watch function being called?
I want both the directives $watch to fire however, I can only get one of them to (i.e. dir-two). Not sure what I need to change. Does it have something to do with isolated scope? Is there a better way of doing this?
AngularJS Code:
var mod = angular.module("myApp", []);
//Controller
mod.controller("myCtrl", function($scope){
$scope.tempformat = "C";
$scope.one="25 - dir-one";
$scope.$watch('tempformat', function(nv){
alert("nv from controller");
});
$scope.two="35 - dir-two";
});
//dir-one directive
mod.directive("dirOne", function(){
return{
restrict: 'E',
template: "<p>{{info}}</p>",
scope: {info: '='
},
link: function (scope, element, attr) {
scope.$watch('tempformat', function(nv){
alert("nv from directive-one");
if(scope.tempformat === "C"){
element.find("p").append("C");
}
else if(scope.tempformat === "F"){
element.find("p").append("F");
}
});
}
}});
//dir-two directive
mod.directive("dirTwo", function($window){
return{
restrict: "EA",
template: "<p></p>",
link: function (scope, element, attr) {
scope.$watch('tempformat', function(nv){
alert("nv from directive-two");
if(scope.tempformat === "C"){
element.find("p").append("C");
}
else if(scope.tempformat === "F"){
element.find("p").append("F");
}
});
}
}
});
HTML Code:
<div ng-app="myApp" ng-controller="myCtrl">
<h2>Temperature</h2>
<input type="radio" ng-model="tempformat" value="C"/> Celcius
<input type="radio" ng-model="tempformat" value="F"/> Farenheit
<dir-one info="one"></dir-one>
<dir-two info="two"></dir-two>
</div>
Does it have something to do with isolated scope?
The problem is the fact that dir-one separates its scope from the parent. There are some alternatives that can be done in this situation such as:
scope.$watch('$parent.tempformat', function(nv){ //...
which will look to the parent for the specified content.
Another alternative is to bind to the directive itself:
scope: {
info: '=',
tempformat: '='
},
and then in the html:
<dir-one info="one" tempformat="tempformat"></dir-one>
see: the documentation for more information. Particularly the Isolating the Scope of a Directive area.
Is there a better way of doing this?
In general isolate scopes help construct reusable components (as noted in the documentation) so if this is something that is being attempted (from the content noted in the answer) then I would support something along the lines of the second option where you can specify that watch content on the directive itself and consider that the "better" way of doing this.
From my experience, and this is solely my own preference, I would bind it to the directive since I usually isolate my scope(s) for a reason.

Is there an alternative to using a directive to create template code?

I had HTML that was duplicated on many screens so I created a directive like this:
app.directive('adminRetrieveButton', ['stateService', function (stateService) {
return {
restrict: 'AE',
template: "<button id='retrieveButton'\
ng-disabled='!home.forms.grid.$pristine'\
ng-click='ctrl.retrieve()' >Retrieve\
<span class='fa fa-fw mlr75'\
ng-class='{\"fa-spin fa-spinner\": stateService.action[Network.Retrieve], \"fa-download\": !stateService.action[Network.Retrieve] }' >\
</span>\
</button>",
link: function (scope, element, attrs) {
scope.stateService = stateService;
}
};
}]);
The directive and other similar ones has no functionality other than to create some template type of code. Now rather than having to code in 8 lines of HTML in each page I just have one line:
<admin-retrieve-button></admin-retrieve-button>
Is there another alternative to this that would make it even simpler without my needing to create a directive?
yes you dont need to create directive only for template you can simply use ng-include
Directives are a powerful tool for working with and modifying the DOM, If you dont manipulating with DOM you dont need directive
There are alternatives, but your method is the best. The way you have it is the correct Angular Way; if you have a standard control, make it a directive.
If you like, you can use the templateUrl option to include the HTML in a separate file instead of inline in the directive configuration:
app.directive('adminRetrieveButton', ['stateService', function (stateService) {
return {
restrict: 'AE',
templateUrl: "adminButton.html",
link: function (scope, element, attrs) {
scope.stateService = stateService;
}
};
}]);
...and in adminButton.html:
<button id="retrieveButton"
ng-disabled="!home.forms.grid.$pristine"
ng-click="ctrl.retrieve()" >Retrieve
<span class="fa fa-fw mlr75"
ng-class="{'fa-spin fa-spinner': stateService.action[Network.Retrieve],
'fa-download': !stateService.action[Network.Retrieve]}">
</span>
</button>
This directive can access its parent scope because it has not been set to isolate scope; if you included a "scope" option on the directive call you'd need to pass in the home and ctrl variables.
You can use ng-include to do this, but your view code will look less semantic and injecting stateService will be a bit more complicated.

Can two side by side directives communicate with each other in AngularJS?

I had such html code
<a href="#" id="someLink">
<i class="someClass"></i><span>Some span text</span>
</a>
<div id="dependsOnSomeLink">
<!-- something here -->
</div>
that I wanted to separate into directives in AngularJS. The logic behind it was, that contents of div is hidden by default and is shown by click on a element.
I wanted to separate that into two directives like that:
angular.module('someModule', [])
.directive('prAbc', function($log) {
return {
require: 'prDef',
restrict: 'E',
template: '<i class="someClass"></i><span>Some span text</span>',
controller: function($scope, $element, $attrs, $transclude) {
// do the harlem share <-- here
},
link: function(scope, iElement, iAttrs, prDef, transcludeFn) {
iElement.bind('click', function() {
prDef.toggleDependentBlock();
});
}
};
})
.directive('prDef', function($log)
{
return {
restrict: 'E',
template: '<div id="dependsOnSomeLink"></div>',
controller: function($scope, $element, $attrs, $transclude) {
$scope.showDependentBlock = false;
this.toggleDependentBlock = function() {
$scope.showDependentBlock = false === $scope.showDependentBlock;
};
}
};
});
and later use it like that
<pr-abc></pr-abc>
<pr-def></pr-def>
But prDef is not defined when it is called from prAbc directive.
require: 'prDef' works when prAbc and prDef are applied on the same DOM element, which is not your case.
What you may need is "parent" directive which will act as a router between the two communicating directives. prDef registers itself to the parent controller and prAbc calls the parent controller which, in turn, calls prDef.
Check out this plucker.
There are at least 3 other options to consider:
Have the parent controller listen to specific events that prAbc emits and broadcasts them downwards, where prDef listens on.
Have the parent controller pass callback functions ('&') to the child directives for registration and routing.
All functions and data is defined on the parent and passed to child directives through binding or prototypal inheritance (essentially, ruin your design :P).
For all 3 options, the parent controller does not have to be defined in a directive, it can be any controller which you apply like:
<div ng-contoller="ParentCtrl">
<pr-abc></pr-abc>
<pr-def></pr-def>
</div>
Your example does not provide enough context to reason which option is the best, but I think the parent directive should be the preferred way to go.
If it helps, checkout how ui-bootstrap's tabs and accordion are implemented. It's a very helpfull example of collaborating, still decoupled, set of directives.

Avoid using extra DOM nodes when using nginclude

I'm struggling to wrap my mind around how to have an ng-include not use an extra DOM element as I'm building an angular app from a plain-HTML demo. I'm working with pretty slim HTML with fully developed, tightly DOM-coupled CSS (built from SASS) and refactoring is something I want to avoid at all costs.
Here's the actual code:
<div id="wrapper">
<header
ng-controller="HeaderController"
data-ng-class="headerType"
data-ng-include="'/templates/base/header.html'">
</header>
<section
ng-controller="SubheaderController"
data-ng-class="subheaderClass"
ng-repeat="subheader in subheaders"
data-ng-include="'/templates/base/subheader.html'">
</section>
<div
class="main"
data-ng-class="mainClass"
data-ng-view>
</div>
</div>
I need <section> to be a repeating element but have its own logic and different content. Both, content and number of repetitions are dependent on business logic. As you can see, putting the ng-controller and the ng-repeat on the <section> element will not work. What would, however, is to insert a new DOM node, which is what I'm trying to avoid.
What am I missing out? Is this best practice or is there a better way?
EDIT: just to clarify as asked in comments, the final HTML I'm trying to generate would be:
<div id="wrapper">
<header>...</header>
<section class="submenuX">
some content from controller A and template B (e.g. <ul>...</ul>)
</section>
<section class="submenuY">
different content from same controller A and template B (e.g. <div>...</div>)
</section>
<section class="submenuZ">
... (number of repetitions is defined in controller A e.g. through some service)
</section>
<div>...</div>
</div>
The reason I want to use the same template B (subheader.html), is for code cleanliness. I conceive subheader.html to have some kind of ng-switch in order to return dynamic content.
But basically, the underlaying quiestion is: is there a way to include the contents of a template transparently, without using a DOM node?
EDIT2: The solution needs to be reusable. =)
Some of the other answers suggest replace:true, but keep in mind that replace:true in templates is marked for deprecation.
Instead, in an answer to a similar question, we find an alternative: It allows you to write:
<div ng-include src="dynamicTemplatePath" include-replace></div>
Custom Directive:
app.directive('includeReplace', function () {
return {
require: 'ngInclude',
restrict: 'A', /* optional */
link: function (scope, el, attrs) {
el.replaceWith(el.children());
}
};
});
(cut'n'paste from the other answer)
Edit: After some research and for the sake of completeness, I've added some info. Since 1.1.4, the following works:
app.directive('include',
function () {
return {
replace: true,
restrict: 'A',
templateUrl: function (element, attr) {
return attr.pfInclude;
}
};
}
);
Usage:
<div include="'path/to/my/template.html'"></div>
There is, however, one gotcha: the template cannot be dynamic (as in, passing a variable through scope because $scope, or any DI for that matter, is not accessible in templateUrl - see this issue), only a string can be passed (just like the html snippet above). To bypass that particular issue, this piece of code should do the trick (kudos to this plunker):
app.directive("include", function ($http, $templateCache, $compile) {
return {
restrict: 'A',
link: function (scope, element, attributes) {
var templateUrl = scope.$eval(attributes.include);
$http.get(templateUrl, {cache: $templateCache}).success(
function (tplContent) {
element.replaceWith($compile(tplContent.data)(scope));
}
);
}
};
});
Usage:
<div include="myTplVariable"></div>
You can create a custom directive, linking to the template with the templateUrl property, and setting replace to true:
app.directive('myDirective', function() {
return {
templateUrl: 'url/to/template',
replace: true,
link: function(scope, elem, attrs) {
}
}
});
That would include the template as-is, without any wrapper element, without any wrapper scope.
For anyone who happens to visit this question:
As of angular 1.1.4+ you can use a function in the templateURL to make it dynamic.
Check out this other answer here
With the right setup, you can define your own ngInclude directive that can run instead of the one provided by Angular.js and prevent the built-in directive to execute ever.
To prevent the Angular-built-in directive from executing is crucial to set the priority of your directive higher than that of the built-in directive (400 for ngInclude and set the terminal property to true.
After that, you need to provide a post-link function that fetches the template and replaces the element's DOM node with the compiled template HTML.
A word of warning: this is rather draconian, you redefine the behavior of ngInclude for your whole application. I therefore set the directive below not on myApp but inside one of my own directives to limit its scope. If you want to use it application-wide, you might want to make its behavior configurable, e.g. only replace the element if a replace attribute is set in the HTML and per default fall back to setting innerHtml.
Also: this might not play well with animations. The code for the original ngInclude-directive is way longer, so if you use animations in your application, c&p the original code and shoehorn the `$element.replaceWith() into that.
var includeDirective = ['$http', '$templateCache', '$sce', '$compile',
function($http, $templateCache, $sce, $compile) {
return {
restrict: 'ECA',
priority: 600,
terminal: true,
link: function(scope, $element, $attr) {
scope.$watch($sce.parseAsResourceUrl($attr.src), function ngIncludeWatchAction(src) {
if (src) {
$http.get(src, {cache: $templateCache}).success(function(response) {
var e =$compile(response)(scope);
$element.replaceWith(e);
});
}
});
}
};
}];
myApp.directive('ngInclude', includeDirective);

Resources