Angular directives - How to select template based on attribute values? - javascript

I am developing a widget where I want to render some messages/text one after another. I want to change the template of the message based on the type of message.
my current directive setup is as follows
directive('cusMsgText', function(){
return {
restrict: 'E',
template:function(elements, attrs){
return '<div></div>';
},
link: function($scope, iElm, iAttrs, controller) {
//add children to iElm based on msg values in $scope
}
};
});
The directive is used as follows
<div ng-repeat="(key, value) in chatUser.msg">
<data-cus-msg-text msg="value.type"></data-cus-msg-text>
</div>
Now my question are -:
Is it possible to return one of multiple strings (templates) from
template function itself based on the actual value of attribute
msg. I tried accessing attrs.msg in template function and it
return value.type.
If not then, Is it good to manipulate template under linker or I
need to move it to compile function?

To render a different template based on value.type you can use the ng-switch statement:
<div ng-switch="value.type">
<div ng-switch-when="type1">
//...template for type 1 here...
</div>
<div ng-switch-when="type2">
//...template for type 2 here...
</div>
</div>
Also, if I understood your second question: manipulation of the uncompiled directive should be done in the compile function, all the manipulation which occurs after compilation should go in the link function.
Docs for ngSwitch
EDIT: +1 to Sebastian for understanding what you wanted. However, what he is proposing is essentially reinventing the wheel, since it is essentially compiling and inserting the template manually (which is what ngSwitch does for you). Also, you can access the attributes you put on your directive through the attrs argument of the link function.

In the template function you don't have access to the scope of your directive. If you want to control what gets rendered you can do this using conditional logic (e.g. ng-switch) in a global template as suggested by simoned or use a link function:
.directive('cusMsgText', function($compile) {
return {
restrict: 'E',
scope: {
msg: '=',
item: '='
},
link: function(scope, element, attrs) {
templates = {
x: '<div>template x {{item.name}}</div>',
y: '<div>template y {{item.name}}</div>'
};
var html = templates[scope.msg];
element.replaceWith($compile(html)(scope));
}
};
});

Related

Add custom Directive to existing Input that already has angular directives [ng-model/ng-required]

I would like to use a standard input control that is decorated with ng-model and ng-required and then add my own custom attribute directive that provides uib-typeahead functionality to the control.
I used this link to get my directive partly working.
Add directives from directive in AngularJS
PLUNKR - The Version 2 of the directive does not work correctly with ng-model
My Directive does add typeahead functionality and that works quite well, but it is not binding the model on to the control after item is selected.
I have two version of my directive.
Version 1: is an element style directive and I have been using it successfully for a while, but it fell short when I wan't to have a bit more control over the input element, especially when I wanted to use ng-required='true' and other ng-message directives.
Version 2: is an attribute style directive, I went with this because I felt it was better to just add the typeahead functionality that I wanted to any standard HTML that can optionally use ng-required='true', ng-model etc...
While this directive is mostly working, it does not interact correctly with ng-model and I'm not sure how to get it working
angular.module(APP)
.directive('wkLocationSuggest', ['$compile', function ($compile) {
return {
restrict: 'A',
require: 'ngModel',
replace: false,
//terminal: true,
//priority: 0,
scope: {
wkApiModel: '=' // Provide access to the internal data that is returned via the API lookup
},
controller: 'LocationSuggestController',
link: function (scope, element, attrs, ngModelCtrl) {
if (!ngModelCtrl) {
return;
}
element.attr('typeahead', 'location as row.location for row in typeAhead($viewValue)');
element.attr('typeahead-wait-ms', '750');
element.attr('typeahead-on-select', 'onSelectInternal($item, $model, $label)');
element.attr('typeahead-min-length', '2');
element.attr('typeahead-focus-first', 'true');
element.removeAttr("wk-location-suggest"); //remove the location-suggest to avoid indefinite loop
element.removeAttr("data-wk-location-suggest"); //also remove the same attribute with data- prefix if it exists
// None of this is working
//// invoked when model changes from the outside
//ngModelCtrl.$render = function () {
// //scope.innerModel = ngModelCtrl.$modelValue;
//};
////// invoked when model changes from the inside
//scope.onChange = function (value) {
// ngModelCtrl.$setViewValue(scope.innerModel);
//};
scope.onSelectInternal = function ($item, $model, $label) {
// This fires, but it effects the ng-model on the first input,
// but not the input that this directive is attached too
ngModelCtrl.$setViewValue($item.location);
};
$compile(element)(scope);
}
};
}]);
These two images demonstrate part of the problem, may be better to test for yourself using PLUNKR above
I initially tried to dynamically add validators to your wk-location-suggest-new directive by implementing blur on the input element in combination with ngModel's $setValidity method; but don't know what exactly was preventing the event from firing.
Therefore, I turned to the other directive wk-location-suggest-old and tweaked it a bit to fit in both desired behaviors.
There, I noticed that you were missing a couple of things:
First of all, in order for a form element to glue with the form itself (wkProfileCompany in your case), and to work with ng-model, the element (in the directive template) needs a name.
Secondly, ng-required (or required) would work with the form only if it is added as an attribute to the element in the directive template, not the directive which compiles to the template containing the element.
Directive Definition
As you may notice, I've passed two properties from the outer scope to the directive's inner scope, namely:
the name of the input element,
and an isRequired flag as to specify whether the input is required or not.
.
.directive('wkLocationSuggestOld', [function () {
return {
restrict: 'E',
require: '?ngModel',
scope: {
name: '#', // <==
isRequired: '=' // <==
},
template: '<input name="{{name}}" type="text" class="{{innerClass}}" ng-model="innerModel"'
+ ' ng-change="onChange()" uib-typeahead="location as row.location for row in typeAhead($viewValue)" '
+ ' typeahead-wait-ms="750" typeahead-on-select="onSelectInternal($item, $model, $label)" '
+ ' typeahead-min-length="2" typeahead-focus-first="true" '
+ ' ng-required="isRequired">', // <== added ng-required here
controller: 'LocationSuggestController',
link: function (scope, element, attrs, ngModel) {
if (!ngModel) {
return;
}
...
}])
HTML
Finally, you can use the tweaked directive in your HTML as such:
<wk-location-suggest-old class="form-control" type="text" name="location2" ng-model="location2" is-required="true"></wk-location-suggest-old>
Plunker
Update
One of the possible reasons for ng-model not correctly binding in the wk-location-suggest-new directive to a provided value (i.e. location3) is that you are replacing the whole DOM element with a new custom DOM element which is compiled with the isolated scope of the directive itself.
Since the directive wk-location-suggest-new has an isolate scope, the scope is totally unaware of location3, because location3 (and all the other location values) are defined in the scope of MainCtrl and NOT the scope of the directive itself; therefore, you'll end up binding the input's value to an undefined property.
link: function (scope, element, attrs, ngModelCtrl) {
if (!ngModelCtrl) {
return;
}
...
$compile(element)(scope); // <== here
You need to update your model in setTimout() like below as you have an isolated scope in the directive.
setTimeout(function () {
scope.$apply(function () {
scope.location3 = 'Your selected value'
});
}, 2000);
Alternatively you can also utilize $timeout service to achieve the same result.

AngularJS ng-model for multiple inputs inside directive

So I basically have a directive with 21 inputs in it. I want to set ng-modelbased on an attribute passed when calling the directive. Now if I do {{myAttr}} inside my directive it outputs the attribute I've set, but if I include it like ng-model="{{myAttr}}" things break.
angular.module('myApp')
.directive('program', function () {
return {
templateUrl: 'app/program/program.html',
restrict: 'EA',
link: function (scope, element, attrs) {
scope.dayModel = 'program.' + attrs.day + 'First';
}
};
});
You use {{variablename}} only in pure html-text, so that angularjs can replace it by the value of your variable at runtime:
http://jsfiddle.net/joshdmiller/hb7lu/
But you would never write ng-form="{{myAttr}}" in an html-tag, instead you have to write ng-form="myAttr" The {{}} serves as a way for angularjs to distinguish between plain text and variable/code. But in html-tags there is no plain text, instead everything is interpreted as variable/code.

Angularjs Multiple directives [gridsection, gridsection] asking for template on: <div gridsection="">

I'm receiving the error: Multiple directives [gridsection, gridsection] asking for templateon : <div gridsection=""> with this code.
I don't see how i'm using nested directives or what is causing this.
html page
<div gridsection ></div>
directive
angular.module('web').directive('gridsection', function() {
return {
restrict: 'A',
replace: false,
scope: {
patient: "=patient"
},
templateUrl: 'directive/section.html',
link: function(scope, element, attrs, fn) {
}
};
});
directive/section.html
<div>
here?
</div>
It seems like you are declaring the gridsection multiple times in your angular code.
I have seen this before when I have a copy of a directive script file in a folder.
i.e. my file structure was
* myDirective.js
* myDirective - copy.js
So essentially I had two directives with the same name.
Doh!
Note originally posted this as a comment but created as an answer in response to comment from #jayjayjay
For posterity, I was getting this exception because I was trying to create a directive named pager and that was colliding with Bootstrap's pager.
Make sure you didn't include the script tag twice.
I had this issue but only declared the directive once in the markup, turns out it was because I included the script twice.
Note: I saw this in one of the comments for another answer and posted it as an answer for easier access/to prevent it getting lost.
I was getting this error for a reason not specified in other answers.
I was using declaration for xyz directive as <xyz xyz="xyz"></xyz>
my definition was:
angular.module('app')
.directive('xyz', function () {
return {
templateUrl: '..../xyz.html',
restrict: 'EA',
scope: {
xyz: '='
},
link: function (scope, element, attrs) {
}
};
});
The problem here is that I allowed directive to be used as element and attribute. so <xyz xyz="xyz"></xyz> contained both the declaration which was causing the issue.
Solution is to either restrict the directive to be used as Element only restrict: 'E' OR change the name of the directive to something like xyzView and use it like <xyz-view xyz="xyz"></xyz-view>.

Angularjs: understanding a recursive directive

I found a great tree directive here. Original: http://jsfiddle.net/n8dPm/
I have been trying to understand the functioning of it through couple of other SO questions, 1,2 . I couldn't quite understand how the recursive calls to render the tree directive work. Mainly the compile function
When all the compile function called?
When is the $compile function cached in the varibale compiledContents (is this the link function?), and when is it appends? Why it is not append always?
--
compile: function(tElement, tAttr) {
var contents = tElement.contents().remove();
var compiledContents;
return function(scope, iElement, iAttr) {
if(!compiledContents) {
compiledContents = $compile(contents);
}
compiledContents(scope, function(clone, scope) {
iElement.append(clone);
});
};
},
The Ng site has some great documentation (some of the best around in my opinion). The overview of the Startup and Runtime loops are very helpful:
http://docs.angularjs.org/guide/concepts
At a high level, when Ng first starts it compiles the DOM starting at where ng-app is located (treated like just another directive by Ng). This means it goes through the elements and looks directives and expressions it needs to link up to the $rootScope (the root of all scopes that are part of the prototypical inheritance chain setup by the compiling/linking process). If it is a directive, the compile process is done on it as well. The compiling process takes all of the Ng directives it finds in the HTML and prioritizes them based on there assigned priority or assumes the priority is zero. When it has them all ordered it executes the compile function for the directive which returns the link function. In the above example there are two show link functions which I will annotate below along with other notes linking it to this explanation. the link function also is given the HTML that was in the element the directive was a attribute, class, or element on in the form of the transclude object.
The link functions are executed which links the scope and the directive along with producing a view. This may include the HTML/transclude so it can be added where the directive ng-transclude is in the template of the directive (which will have the same process applied to it with it's template being the transclude).
So here are my notes for the slightly corrected custom directive above:
module.directive("tree", function($compile) {
//Here is the Directive Definition Object being returned
//which is one of the two options for creating a custom directive
//http://docs.angularjs.org/guide/directive
return {
restrict: "E",
//We are stating here the HTML in the element the directive is applied to is going to be given to
//the template with a ng-transclude directive to be compiled when processing the directive
transclude: true,
scope: {family: '='},
template:
'<ul>' +
//Here we have one of the ng-transclude directives that will be give the HTML in the
//element the directive is applied to
'<li ng-transclude></li>' +
'<li ng-repeat="child in family.children">' +
//Here is another ng-transclude directive which will be given the same transclude HTML as
//above instance
//Notice that there is also another directive, 'tree', which is same type of directive this
//template belongs to. So the directive in the template will handle the ng-transclude
//applied to the div as the transclude for the recursive compile call to the tree
//directive. The recursion will end when the ng-repeat above has no children to
//walkthrough. In other words, when we hit a leaf.
'<tree family="child"><div ng-transclude></div></tree>' +
'</li>' +
'</ul>',
compile: function(tElement, tAttr, transclude) {
//We are removing the contents/innerHTML from the element we are going to be applying the
//directive to and saving it to adding it below to the $compile call as the template
var contents = tElement.contents().remove();
var compiledContents;
return function(scope, iElement, iAttr) {
if(!compiledContents) {
//Get the link function with the contents frome top level template with
//the transclude
compiledContents = $compile(contents, transclude);
}
//Call the link function to link the given scope and
//a Clone Attach Function, http://docs.angularjs.org/api/ng.$compile :
// "Calling the linking function returns the element of the template.
// It is either the original element passed in,
// or the clone of the element if the cloneAttachFn is provided."
compiledContents(scope, function(clone, scope) {
//Appending the cloned template to the instance element, "iElement",
//on which the directive is to used.
iElement.append(clone);
});
};
}
};
});
Whole thing working:
http://jsfiddle.net/DsvX6/7/

AngularJs: create directive

I've a question about a directive I want to write. Here is the jsfiddle
This is the HTML
<div ng-controller="TestCtrl">
<mydir base="{{absolute}}">
<div>{{path1}}</div>
<div>{{path2}}</div>
</mydir>
</div>
The directive, called 'mydir', will need the value of the base attribute and all the path values (there can be any number of paths defined). I don't want to inject values from the controller directly into the directive scope (they need to be independent). I have 3 questions about this (checkout the jsfiddle too!)
1) although the code works as it is now, there is an error: "TypeError: Object # has no method 'setAttribute'". Any suggestions what is wrong?
2) in the directive scope.base is 'undefined' instead of '/base/'. Can this be fixed.
3) How can I make a list of all the path values in the directive ?
You need to set an isolate scope that declares the base attribute for this directive:
scope: {
base: "#"
}
This solves (2).
Problem (1) is caused from the replace: true in cooperation with the scope above, because it creates a comment node to contain the repeated <div>s.
For (3), add paths: "=" to scope and, of ocurse, pass them from the parent controller as an array.
See forked fiddle: http://jsfiddle.net/swYAZ/1/
You're creating an new scope in your directive by setting scope: true what you want to do is:
app.directive('mydir', function ($compile, $rootScope) {
return {
replace: true,
restrict: 'E',
template: '<div ng-repeat="p in paths"> {{base}}{{p}}/{{$index}}</div>',
scope: {
base : '='
},
link: function (scope, iElement, iAttrs) {
scope.paths = ['p1', 'p2'] ;
}
}
});
This will setup a bi-directional binding between your directive's local scope and the parent's scope. If the value changes in the parent controller it will change in the directive as well.
http://jsfiddle.net/4LAjf/8/
If you want to make a robust directive to be able to replace the data to its content (3)
You can pass a reference as an attribute and display the data accordingly.
See this article, look for wrapper widget.

Categories

Resources