AngularJS : Initializing isolated scope inside a directive - angularjs

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

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.

When to declare function with 'scope' vs. 'var' in directive?

In this plunk I have a directive that is instantiated twice. In each case, the result of the directive (as defined by its template) is displayed correctly.
Question is whether getValue2() needs to be defined as scope.getValue2 or var getValue2. When to use each in the directive?
HTML
instance 1 = <div dirx varx="1"></div>
<br/>
instance 2 = <div dirx varx="2"></div>
Javascript
var app = angular.module('app', []);
app.controller('myCtl', function($scope) {
});
app.directive('dirx', function () {
var directive = {};
directive.restrict = 'EA';
directive.scope = {
varx: '='
};
directive.template = '{{getValue()}}';
directive.link = function (scope, element, attrs) {
scope.getValue = function(){
return getValue2();
};
var getValue2 = function() {
return scope.varx * 3;
}
};
return directive;
});
The only time you need declare something as a property on the $scope object is when it is part of your application state.
Angular 1.x will "dirty check" the $scope and make changes to the DOM. Anything on the $scope object can be watched, so you can observe the variable and trigger functions. This is why Angular searching & filtering can be done with almost no JS code at all. That being said, it's generally good practice to keep the '$scope' free of anything that isn't needed.
So as far as getValue() is concerned, is it being called on render or in a directive in your HTML? if the answer is "no", then it doesn't need to be declared as a property on the $scope object.
Since you're using getValue() in the directive template, it is being rendered in the UI and needs to be in Angular's $scope.
You can also just do:
directive.template = '{{ varx * 3 }}';
docs: https://docs.angularjs.org/guide/scope
The first thing is that the code contains unnecessary nested calls. it can be:
var getValue2 = function() {
return scope.varx * 3;
}
scope.getValue = getValue2;
The second thing is that getValue2 isn't reused and isn't needed, it can be:
scope.getValue = function() {
return scope.varx * 3;
}
Since getValue is used in template, it should be exposed to scope as scope.getValue. Even if it wouldn't be used in template, it's a good practice to expose functions to scope for testability. So if there's a real need for getValue2, defining and calling it as scope.getValue2 provides small overhead but improves testability.
Notice that the use of link function and direct access to scope object properties is an obsolete practice, while up-to-date approach involves controllerAs and this.

setting scope to null blocks 2 way binding angular directive

I have created a directive and I believe that two-way bind is being broken when I set a bound scope variable (textStyleOriginal) to null. What is a good way to resolve this issue?
.directive('textStylePalette', function($log, toastr, _){
return {
restrict: 'E',
templateUrl: 'app/palettes/text/text-style-palette.html',
scope: {
textStyleOriginal: '=textStyle'
},
link: textPaletteLinkFn
};
function textPaletteLinkFn(scope, elem, attr) {
scope._ = _;
scope.textStyle = null;
// Used when closing the palette
scope.deselectStyle = function() {
// I BELIEVE THE PROBLEM IS THE NEXT LINE
scope.textStyleOriginal = null;
scope.textStyle = null;
};
...
// THIS WATCH STOPS WORKING.
scope.$watch('textStyleOriginal', function(newVal, oldVal){
$log.debug('n: ' + newVal + '|o: ' + oldVal );
debugger;
if (newVal && newVal !== oldVal) {
...
}
});
}
The html where the binding is initially connected is as follows:
<text-style-palette ng-show="selectedStyle !== null" text-style="selectedStyle">
</text-style-palette>
I think I know what's the problem.
Since you have an isolated scope, you'll have the textStyleOriginal set from the parent scope. It means that if you override it with the value null, then you'll loose the reference to the original object.
E.g. even when you modify your textStyleOriginal in your parent scope, it won't take any effect in your directive, since you lost the reference to it already.
A few minutes after I asked the question I tried something that seemed to work. Leaving this question up to document my answer:
It was basically the simple, 'always make passed scope variables as part of an object'.
I made some changes so that the external selectedStyle that was feeding the directives was part of an object. here's the code
<cm-text-style-palette ng-show="selections.selectedStyle !== null" text-style="selections.selectedStyle">
</cm-text-style-palette>
Notice that it's selections.selectedStyle not just selectedStyle.
The issue has to do with how variable pointing works. For more details this video might help: https://egghead.io/lessons/angularjs-the-dot#/tab-transcript
Best of luck with your projects!

Dynamic templateUrl - 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 (...); }
})();

In an AngularJS directive, how do I set a parent controller's property?

Here's a jsFiddle that shows what I'm trying to do: http://jsfiddle.net/P3c7c
I'm using the Google Places AutoComplete widget to obtain lat/long coordinates, which I then wish to use in a subsequent search function. It seemed that the proper way to implement this, considering the need to add an event listener to an element, was to use a directive, and to attach the listener using the directive's linking function. However, inside of this listener, I need it to set the location property of the SearchForm controller, which is its parent. And I have not figured out how to make that connection. Here's the relevant chunk of code:
/* Controllers */
function SearchForm($scope){
$scope.location = ''; // <-- this is the prop I wish to update from within the directive
$scope.doSearch = function(){
if($scope.location === ''){
alert('Directive did not update the location property in parent controller.');
} else {
alert('Yay. Location: ' + $scope.location);
}
};
}
/* Directives */
angular.module('OtdDirectives', []).
directive('googlePlaces', function(){
return {
restrict:'E',
replace:true,
transclude:true,
scope: {location:'=location'}, // <--prob something wrong here? i tried #/& too, no luck
template: '<input id="google_places_ac" name="google_places_ac" type="text" class="input-block-level"/>',
link: function($scope, elm, attrs, ctrl){
var autocomplete = new google.maps.places.Autocomplete($("#google_places_ac")[0], {});
google.maps.event.addListener(autocomplete, 'place_changed', function() {
var place = autocomplete.getPlace();
// THIS IS THE STRING I WANT TO SAVE TO THE PARENT CONTROLLER
var location = place.geometry.location.lat() + ',' + place.geometry.location.lng();
// THIS IS NOT DOING WHAT I EXPECT IT TO DO:
$scope.location = location;
});
}
}
});
Thanks in advance.
Two minor corrections and it should work:
<google-places location="location"></google-places>
and when you set location inside your directive you also need to do $scope.$apply()
$scope.$apply(function() {
$scope.location = location;
});
You have to do $apply() because the event happens outside of angular digest loop, so you have to let angular know that something has changed inside the scope and it needs to "digest" it's bi-directional bindings and other internal async stuff.
Also, I don't think you need transclude:true.
http://jsfiddle.net/P3c7c/1/

Resources