Dynamic templateUrl - AngularJS - angularjs

So as of Angular 1.1.4, you can have a dynamic template url. From here,
templateUrl - Same as template but the template is loaded from the specified URL. Because the template loading is asynchronous the compilation/linking is suspended until the template is loaded.
You can specify templateUrl as a string representing the URL or as a function which takes two arguments tElement and tAttrs (described in the compile function api below) and returns a string value representing the url.
How can I utilize this to generate a dynamic template based on, say, an attribute on my directive? Obviously this doesn't work, since tAttrs.templateType is simply the string "templateType"
templateUrl: function (tElement, tAttrs) {
if (tAttrs.templateType == 'search') {
return '/b/js/vendor/angular-ui/template/typeahead/typeahead.html'
} else {
return '/b/js/vendor/angular-ui/template/typeahead/typeahead2.html'
}
}
Given that I don't have access to the scope, how do I manage this?

The following is also possible for creating dynamic templates in AngularJS:
In your directive use:
template : '<div ng-include="getTemplateUrl()"></div>'
Now your controller may decide which template to use:
$scope.getTemplateUrl = function() {
return '/template/angular/search';
};
Because you have access to your scope parameters, you could also do:
$scope.getTemplateUrl = function() {
return '/template/angular/search/' + $scope.query;
};
So your server could create a dynamic template for you.

templateUrl: function (elem, attrs) {
return attrs["template"] == "table" ?
"tableTemplate.html" : "itemTemplate.html";
}

So the issue was with how I hacked the typeahead directive ... I was setting a scope variable on the typeahead, to be evaluated on the typeaheadPopup directive. Instead, I just passed the templateType attr directly as string & evaluated that. E.g.
var popUpEl = angular.element(
"<typeahead-popup " +
"matches='matches' " +
"active='activeIdx' " +
"select='select(activeIdx)' " +
"template-type='" + attrs.templateType + "'" +
"query='query' " +
"position='position'>" +
"</typeahead-popup>");
Instead of "template-type='templateType'"

Ran into a similar issue when creating a file upload fallback for browsers that don't support the File API (< IE10). Key difference is I needed the page to intelligently decide which template to display without the benefit of an attribute value to switch on.
I ended up using the constant provider for my directive. Constants basically set up default parameters that can be injected anywhere in your directive. I simply let the constant call a function to determine browser support, then reference that value when I need to determine which template to pull. This is nice since 1) there's no attribute to reference and 2) it's available during the pre-link phase when you don't have access to the controller.
(function () {
var myDir = angular.module('myDir', []);
myDir.constant('myDirConfig', {
hasFileSupport: fileApiIsSupported()
});
myDir.directive('myDir', ['myDirConfig', function (myDirConfig) {
return {
templateUrl: function () {
if (myDirConfig.hasFileSupport) {
return 'pathToTemplate/html5.html';
} else {
return 'pathToTemplate/fallback.html';
}
}
};
}];
function fileApiIsSupported() { return (...); }
})();

Related

How to modify an AngularJs directive template based on the value of attribute?

I have a dropdown directive built on top of Angular Bootstrap typeahead.
I want the consumer of the directive to be able to supply an attribute (limit-to-list) which determines whether or not user input is limited to list members. In the uib-typeahead directive, this is achieved by setting the typeahead-editable attribute "true" or "false"
Because my directive encapsulates the uib which generates the dropdown, I need to change the template of my directive to change its behavior accordingly, but I can't figure out how that can be done. I tried to modify the string template in the return clause of my directive, but that does not work, I guess because the value of the template is read before the return function is processed?
Here is the directive:
angular.module("app").directive("dropDown", function () {
var mt=mydropdowntemplate;
return {
link: function (scope, element, attrs) {
var limitToList = attrs["limit-to-list"]=="false";
var editable = !limitToList;
if (editable) {
mt=mt.replace("typeahead-editable='false'","typeahead-editiable='true'");
}
console.log("template: " + mt )
var list = scope[attrs["list"]];
var length=list.length
var valueName = attrs["value"];
var idName = attrs["key"];
},
template: mt //This has the value of mt prior to the replace function above
}
})
By looking at the page, I can see that the actual template used was the one before the change applied in the result block.
Plunker link
The link function is an inappropriate place to modify the template as it is executed after the template is compiled. Instead use the function form of the template property
to modify the template:
angular.module("app").directive("dropDown", function () {
var mt=mydropdowntemplate;
return {
link: function (scope, element, attrs) {
var list = scope.$eval(attrs.list);
var length=list.length
var valueName = attr.value;
var idName = attrs.key;
},
template: function (tElem, tAttrs) {
var limitToList = tAttrs.limitToList=="false";
var editable = !limitToList;
if (editable) {
mt=mt.replace("typeahead-editable='false'","typeahead-editiable='true'");
}
console.log("template: " + mt )
return mt;
}
}
})
For more information, see AngularJS Comprehensive Directive API Reference - template.
Use 'scope' property of the returned object to pass data from through attributes.
Like so, you can use 'bindToController" property, if you use controllerAs syntax.
Well i'm strongly recommend to use component approach in replace to directive.

Is it possible to populate directives in a page dynamically

I have a list directive selectors in an array I need to populate them dynamically in a page at runtime how do I proceed I am using angularjs 1.5.8
You should use $compile service to do that. For example, you have the following array of directives:
var myDirectives = [
"<my-app-directive-one></my-app-directive-one>",
"<my-app-directive-two></my-app-directive-two>",
"<my-app-directive-three></my-app-directive-three>"
];
Then you can compile them individually or in a loop and insert to you html like this:
TestDirective.$inject = ["$compile"];
function TestDirective($compile) {
return {
restrict: "E",
link: link
};
function link(scope, element) {
element.append($compile(myDirectives[0])(scope));
}
}
I used the scope of a parent directive (used as a directive sandbox), but you can create a child scope (scope.$new()) or an isolated scope (scope.$new(false)) for it.
If you array looks like this:
var myDirectives = ["my-app-directive-one", "my-app-directive-two"];
then you can simply do the following transformation:
myDirectives = myDirectives.map(
function(directive) {
return "<" + directive + "></" + directive + ">";
}
);
Simpler solution
You can just use ng-if instead of ng-show, if you can place all the directives on the same page before runtime. It will solve you perfomance issues, since ng-if removes all the watchers when destroying a directive.

Angular directives with a shared controller don't share the controller's $scope

If I have two different directives with isolated scopes, and they share a controller, shouldn't they share the controller's $scope?
Effectively, I have the following scenario:
I call to the server and pass it some parameters, let's call them x and y.
The server either returns some data, or it returns null which indicates that this data is not available.
If the data is returned, I need to display a button; if the user clicks the button, it will display the data in a separate div.
If the data is not returned, I do not display the button.
I've been trying to implement this as a set of linked directives. I'm trying to do it this way because this "component" is to be re-used multiple times in the application, and I'm trying to do this in two directives because I couldn't figure out any other way to make a single directive control two different elements.
In addition to there being many, they need to be linked by x and y values. So if I have a button with particular x and y, then clicking on it will only toggle the visibility of the display area with the same x and y values. Effectively a given view will have multiple of each with different x and y values.
Anyway, I have a potential working solution, but I seem to be having problems with the shared scope. When I click the button, I have logging statements which correctly show that we trigger the "show" logic. But the logging statements in the ng-if of the div consistently evaluate the same logic to false and doesn't display.
My solution is in three parts:
A directive for the button
A directive for the "display"
A controller that is shared by the two
I have a trivial working example, which I will share below. There's a Plunkr URL at the end of this post as well.
Here is the HTML. The <p> tag in the middle is just to demonstrate that the two directives are physically not adjacent.
<trigger x="apple" y="2" ></trigger>
<p>Some unrelated dom content...</p>
<display x="apple" y="2"></display>
This is the trigger directive, which is the button:
app.directive("trigger", function() {
return {
restrict: "E",
scope: {
x : "#",
y : "#"
},
transclude: false,
template: "<button ng-if='hasCalculation(x,y)' ng-click='toggle()'>Trigger x={{x}} & y={{y}}</button>",
controller: 'testController',
link: function(scope) {
scope.doSomeWork();
}
};
});
This is the display directive, which is supposed to show the data when toggled by the button:
app.directive("display", function() {
return {
restrict: 'E',
scope: {
x : '#',
y : '#'
},
require: '^trigger',
transclude: false,
controller: 'testController',
template: "<p ng-if='shouldShow(x,y)'>{{getCalculation(x,y)}}</p>"
};
});
This is the shared controller, testController:
app.controller("testController", ["$scope", function($scope) {
$scope.shouldShow = [[]];
$scope.calculatedWork = [[]];
$scope.doSomeWork = function() {
var workResult = "We called the server and calculated something asynchonously for x=" + $scope.x + " and y=" + $scope.y;
if(!$scope.calculatedWork[$scope.x]) {
$scope.calculatedWork[$scope.x] = [];
}
$scope.calculatedWork[$scope.x][$scope.y] = workResult;
};
$scope.hasCalculation = function(myX, myY) {
var xRes = $scope.calculatedWork[myX];
if(!xRes) {
return false;
}
return $scope.calculatedWork[myX][myY]
}
$scope.toggle = function() {
if(!$scope.shouldShow[$scope.x]) {
$scope.shouldShow[$scope.x] = [];
}
$scope.shouldShow[$scope.x][$scope.y] = !$scope.shouldShow[$scope.x][$scope.y];
console.debug("Showing? " + $scope.shouldShow[$scope.x][$scope.y]);
}
$scope.isVisible = function(myX, myY) {
console.debug("Checking if we should show for " + myX + " and " + myY);
var willShow;
if(!$scope.shouldShow[myX]) {
willShow = false;
} else {
willShow = $scope.shouldShow[myX][myY];
}
console.debug("Will we show? " + willShow);
return willShow;
}
$scope.getCalculation = function(myX, myY) {
if(!$scope.calculatedWork[myX]) {
return null;
}
return $scope.calculatedWork[myX][myY];
}
}]);
Here is the Plunkr.
If you go to the Plunkr, you'll see the trigger button correctly rendered. If you click the button, you'll see that the toggle() method correctly flips the value of the shouldShow to the opposite of what it was previously (there's a console debug statement in that method that shows its result). You'll also see the re-evaluation of the isVisible method which determines if the display should show -- this always returns false.
I think it has to do with the fact that I'm saving my data and visibility state relative to $scope. My understanding of this is that each individual directive has its own $scope, but since they share a controller, shouldn't they share that controller's $scope?
An isolate scope does not prototypically inherit the properties of the parent scope.
In AngularJS, a child scope normally prototypically inherits from its parent scope. One exception to this rule is a directive that uses scope: { ... } -- this creates an "isolate" scope that does not prototypically inherit.(and directive with transclusion) This construct is often used when creating a "reusable component" directive. In directives, the parent scope is used directly by default, which means that whatever you change in your directive that comes from the parent scope will also change in the parent scope. If you set scope:true (instead of scope: { ... }), then prototypical inheritance will be used for that directive.
Source: https://github.com/angular/angular.js/wiki/Understanding-Scopes
Additionally, a controller is not a singleton it is a class...so two directives can't share a controller. They each instantiate the controller.
If you want two controllers to share the same data, use a factory.

AngularJS : Initializing isolated scope inside a directive

I have created a directive that accepts some attributes and initializes the isolated scope with these attributes. If an attribute isn't specified, then the isolated scope should be initialized with a calculated value.
I added a link function that inspects the scope and initializes the default values (if no value has been set using the attributes). The scope has been initialized, but if I set a default value then it will be overwritten later by the framework.
A workaround is to use $timeout(...) and set it afterwards, but this seems too much of a hack.
function ($timeout) {
return {
scope: { msg1: '#', msg2: '#' },
template: '<div>{{msg1}} {{msg2}} {{msg3}}</div>',
link: function ($scope, $elt, $attr) {
var action = function() {
if (!$scope.msg2) $scope.msg1 = 'msg1';
if (!$scope.msg2) $scope.msg2 = 'msg2';
if (!$scope.msg3) $scope.msg3 = 'msg3';
};
action();
//$timeout(action, 0);
}
};
});
I have prepared a JSFiddle to illustrate what is happening.
msg1 is initialized via the attribute and has the correct value at all times.
msg2 is not initialized via an attribute, but can be set using an attribute. This value is overwritten after the link method has been called.
msg3 is not initialized via an attribute, and this isn't even possible. This value is set when constructing the controller and works fine.
It seems that AngularJS creates the scope and updates its value after the controller is created and the directive is linked into the DOM. Can anyone tell me the recommended way to do this?
You have to operate on the attributes themselves if you want to set defaults for '#' type binding. Read about $compile
You can do it in the compile function:
compile: function(element, attrs) {
if (!attrs.msg1) attrs.msg1 = 'msg1';
if (!attrs.msg2) attrs.msg2 = 'msg2';
}
http://jsfiddle.net/5kUQs/9/
OR you can use the link function as well.
link: function ($scope, $elt, attrs) {
var action = function() {
console.log('msg1:' + $scope.msg1 + ', msg2:' + $scope.msg2 + ', msg3: ' + $scope.msg3);
if (!attrs.msg1) attrs.msg1 = 'msg1';
if (!attrs.msg2) attrs.msg2 = 'msg2';
if (!attrs.msg3) attrs.msg3 = 'msg3';
};
action();
}
http://jsfiddle.net/5kUQs/13/
The reason for this is that there is a binding with the attribute setup which overrides your changes to that scope variable. We need to modify the attribute that the value is being taken from.
# or #attr - bind a local scope property to the value of DOM
attribute. The result is always a string since DOM attributes are
strings
You can try initializing your $scope attributes in the controller of the directive, rather than the linking function. When the controller is initialized, the scope should already be set.
I know this is an old one but I came across it looking for an answer and while I didn't get it, I did update your fiddle to get this example working.
function MyController($scope){
var c = this;
c.msg1 = $scope.msg1;
c.msg2 = $scope.msg2;
var action = function() {
console.log('msg1:' + $scope.msg1 + ', msg2:' + $scope.msg2 + ', msg3: ' + $scope.msg3);
if (!c.msg2) c.msg1 = 'msg1';
if (!c.msg2) c.msg2 = 'msg2';
if (!c.msg3) c.msg3 = 'msg3';
};
action();
};

Is it possible to provide an implicit way of converting object into string for angular templates?

Let's assume I've got a few objects that have the same prototype and I want to customize their display in Angular template. I know I can create my own filter, and then use it like that:
<p>{{anObjectOfProtoP | myCustomFilter}}</p>
or a function attached to $scope:
<p>{{myCustomFunction(anotherObjectOfProtoP)}}</p>
My question is: is it possible to achieve similar functionality without explicitly specifying the rendering function every time? The ideal solution would be if angular checked for function toAngularString on object inside the {{}}, and then used it's return value in template.
In other words, I'd like Angular to do
function (o) {
if (typeof o.toAngularString === 'function') return o.toAngularString();
return o;
}
on every object inside {{}}.
Depending on whether you use {{ ... }} or ng-bind syntax, the .toJSON and the .toString function on your object will be called to determine its representation. Hence, you can provide the representation you want in either .toString or .toJSON function of your object.
This discrepancy in which function is called has let to some problems, actually.
Another way you can do that is by writing your own directive my-toangularstr as this:
app.directive('myToangularstr', function () {
return {
scope: true,
template: '<span class="my-angular-value">{{ val.toAngularString() }}</span>',
link: function (scope, elem, attrs) {
scope.$watch(attrs['myToangularstr'], function (newVal) {
if (typeof newVal !== 'undefined') {
scope.val = newVal;
}
})
}
}
})
A working demo showing all three methods is here.
I think that is as close as one can get using the external API of angular.

Resources