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

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.

Related

How to access parent's controller function from within a custom directive using *parent's* ControllerAs?

I'm in need of building a transformation directive that transforms custom directives into html.
Input like: <link text="hello world"></link>
should output to: <a class="someclass" ng-click="linkClicked('hello world')"></a>
linkClicked should be called on the parent controller of the directive.
It would have been very easy if I was the one responsible for the html holding the 'link' directive (using isolated scope), but I'm not. It's an as-is input and I have to figure a way to still do it.
There are countless examples on how to do similar bindings using the default scope of the directive, but I'm writing my controllers using John Papa's recommendations with controllerAs, but don't want to create another instance on the controller in the directive.
This is what I have reached so far:
(function () {
'use strict';
angular
.module('app')
.directive('link', link);
link.$inject = ['$compile'];
function link($compile) {
return {
restrict: 'E',
replace: true,
template: '<a class="someclass"></a>',
terminal: true,
priority: 1000,
link: function (scope, element, attributes) {
element.removeAttr('link'); // Remove the attribute to avoid indefinite loop.
element.attr('ng-click', 'linkClicked(\'' + attributes.text + '\')');
$compile(element)(scope);
},
};
}
})();
$scope.linkClicked = function(text){...} in the parent controller works.
element.attr('ng-click', 'abc.linkClicked(..)') in the directive (where the parent's controllerAs is abc) - also works.
The problem is I don't know which controller will use my directive and can't hard-code the 'abc' name in it.
What do you suggest I should be doing?
It's difficult to understand from your question all the constraints that you are facing, but if the only HTML you get is:
<link text="some text">
and you need to generate a call to some function, then the function must either be:
assumed by the directive, or
conveyed to the directive
#1 is problematic because the user of the directive now needs to understand its internals. Still, it's possible if you assume that a function name is linkClicked (or whatever you want to call it), and the user of your directive would have to take special care to alias the function he really needs (could be done with "controllerAs" as well):
<div ng-controller="FooCtrl as foo" ng-init="linkClicked = foo.actualFunctionOfFoo">
...
<link text="some text">
...
</div>
app.directive("link", function($compile){
return {
transclude: "element", // remove the entire element
link: function(scope, element, attrs, ctrl){
var template = '<a class="someclass" ng-click="linkClicked(\'' +
attrs.text +
'\')">link</a>';
$compile(template)(scope, function(clone){
element.after(clone);
});
}
};
});
Demo
#2 is typically achieved via attributes, which isn't possible in your case. But you could also create a sort of "proxy" directive - let's call it onLinkClick - that could execute whatever expression you need:
<div ng-controller="FooCtrl as foo"
on-link-click="foo.actualFunctionOfFoo($data)">
...
<link text="some text">
...
</div>
The link directive now needs to require: "onLinkClick":
app.directive("link", function($compile){
return {
transclude: "element", // remove the entire element
scope: true,
require: "?^onLinkClick",
link: function(scope, element, attrs, ctrl){
if (!ctrl) return;
var template = '<a class="someclass" ng-click="localClick()">link</a>';
scope.localClick = function(){
ctrl.externalFn(attrs.text);
};
$compile(template)(scope, function(clone){
element.after(clone);
});
}
};
});
app.directive("onLinkClick", function($parse){
return {
restrict: "A",
controller: function($scope, $attrs){
var ctrl = this;
var expr = $parse($attrs.onLinkClick);
ctrl.externalFn = function(data){
expr($scope, {$data: data});
};
},
};
});
Demo
Notice that having a link directive would also execute on <link> inside <head>. So, make attempts to detect it and skip everything. For the demo purposes, I used a directive called blink to avoid this issue.

Multiple ngIncludes with Different controls visible

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>

Different scope when using ng-repeat and custom directive in one element

I have a directive like:
angular.module('myApp').directive('myDirective', function() {
return {
templateUrl: '/views/myView.html',
restrict: 'E',
link: function (scope, element, attrs) {
console.log(scope);
console.log(angular.element(element).scope());
}
};
});
the template html is:
<div>{{item.text}}</div>
and view html is:
<div ng-init="items=[{text:'hello'}, {text: 'world'}]">
<my-directive ng-repeat="item in items"></my-directive>
</div>
I find the scope and angular.element(element).scope() is not the same one, but I have to use angular.element(element).scope() way to get the scope of ngRepeat-item somewhere else.
Am I misunderstanding something?
Note:
The code could be reproduced only when including jQuery.
Here is related github issue.
The main reason is: angular1.2.x does not support jQuery2.0.x, and angular1.3.x will do.
So the solution here will be:
angular1.2.x + jQuery 1.x
angular1.3.x + jQuery 2.x
angularjs only

scope change in a directive not a apply in view

I have a directive in a template.html, included by a ng-include, in this directive I change the scope , but it is not change in my view
Here is my html
<div ng-controller="myCtrl">
<div id="modal">
<div ng-show="showDIv">Somthing to controll</div>
</div>
<div ng-include src="template.html">
</div>
Here is my template
<a ng-support></a>
And here is my directive
app.directive('ngSupport', function(){
return {
restrict: 'A',
link: function(scope, elem, attr, ctrl) {
elem.bind('click', function(e) {
$("#modal").dialog({height:518,width:900,modal:true });
scope.showDiv = true;
scope.$apply();
});
}
};
});
When i change the scope in the directive it is not apply in the view, anyone could help please ?
ng-include creates a new scope so scope.showDiv only affects the local scope.
Depending on how you want to structure your application, you could try accessing scope.$parent.showDiv instead, but it is not really future proof as it will depend on the HTML nesting.
A better solution would be to have the showDiv property stored inside an object in the parent scope. For example scope.ui = {}, this way, when you set scope.ui.showDiv = true in your directive, it will look up the parent scope automatically (using prototype inheritance), instead of adding the property to the local scope.
Finally, another solution would be to refactor your code to make it less complex: I think using a ng-include just for adding one element is an overkill, you could put directly <a ng-support></a> inside your html, which would avoid the problem you have with an intermediary scope being generated.
Another option is to broadcast an event, and watch for it in the controller. Something like this.
app.directive('ngSupport', function($rootScope){
return {
restrict: 'A',
link: function(scope, elem, attr, ctrl) {
elem.bind('click', function(e) {
$("#modal").dialog({height:518,width:900,modal:true });
$rootScope.$broadcast('event:modal-clicked');
});
}
};
});
With this in your controller.
$scope.$on('event:modal-clicked', function() {
$scope.showDiv = true;
});

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