angular directive with isolate scope, fields inaccessible - angularjs

I'm trying to write a directive which takes a scope variable name and assigns to it the result of passing a different named parameter into a function. Below, the files="result" is intended to create a {{result}} variable in the glob isolate scope. The contents of the "matching" variable are to be evaluated in the parent context, and assigned to an isolate 'matching' variable.
the directive then calls a function eventually assigning to the isolate variable pointed to by files (result here) the array returned. expansion of {{result}} could then be used for example in an ng-repeat.
The directive should be reusable without changing the variable names.
This isn't happening. If I assign everything to a parent, I can get it working but need to change the variable names each time.
angular.module('j20-glob', ['api'])
/*
* usage: <glob files="result" matching="/bin/{{prefix}}*">
* {{result}}
* </glob>
* should allow another just after the first without stomping result
* <glob files="result" matching="/something">{{result}}</glob>
*/
.directive('glob', ['$parse', 'api', function($parse, $api) {
return {
priority: 99, // it needs to run after the attributes are interpolated
restrict: 'AE',
scope: {
},
link: function(scope, iElement, iAttributes) {
var indexModel = $parse(iAttributes.files);
iAttributes.$observe('matching', function(value) {
if (!value)
return;
$api.glob(value).then(function(res) {
indexModel.assign(scope, res);
// indexModel.assign(scope.$parent, res);
});
});
}
}
}
]);

If I understand your code here, you are having a similar issue to what I answered here: Directive doesn't work when I which the version of Angular to 1.0.1 to 1.2.27.
You have created an Element Directive, called glob. The Directive has an Isolate Scope, which you attach a property, result in your example. This all works fine. The problem is, the property in the isolate scope is only accessible within the directive; and in your case, you are trying to access it outside the directive.
The Element <glob></glob> is your directive. This Element can be containers for other Elements, for example an angular expression {{result}} but these Elements are not part of the Directive, and therefore not scoped in the isolate.
If you were to include a template, and place {{result}} inside the template, you would see the expected result. However, this stops working if you change the variable you are passing in.
A rough draft of a working Directive using a transclude function might be something like:
.directive('glob', ['$parse', 'api', function($parse, $api) {
return {
priority: 99, // it needs to run after the attributes are interpolated
restrict: 'AE',
scope: {
},
transclude : true,
link: function(scope, iElement, iAttributes, ctrl, transclude) {
var indexModel = $parse(iAttributes.files);
iAttributes.$observe('matching', function(value) {
if (!value)
return;
$api.glob(value).then(function(res) {
indexModel.assign(scope, res);
// indexModel.assign(scope.$parent, res);
});
//append our scope into the DOM element (clone) instead of $scope
transclude(scope, function(clone, scope){
element.append(clone);
});
});
}
}
}
]);

Related

Call Parent Controller's Function from Directive

I'm trying to call a parent controller's function from a custom directive upon choosing an option from a select dropdown, but it keeps giving me ng:cpws error.
Error: [ng:cpws] http://errors.angularjs.org/1.4.8/ng/cpws
HTML:
<select chosen ng-model="selectedValue.recipientCountry" ng-options="x.Name.toLowerCase() for x in data.RecipientCountries" >
<option value=""></option>
</select>
JS:
uiComponentsModule.directive('chosen', [ function () {
return {
link: function(scope, element, attrs) {
scope.$watch(function (newVal, oldVal) {
scope.$parent.selectRecipientCountry(x)
})
}
}
}])
I'm trying to modify someone else's code, so it's hard to know exactly what's going on.
You need to pass the function to the directive from the parent controller, like below (I am free-handing without testing because you didn't provide a plunk, so I am sure you will need to adjust the code. The point is that the function gets passed as a param. If you don't know how to pass variables to directives from the parent controller, you won't understand this, so read up on that first). Note that I added 'scope' to your directive - that's where you define params for your directive, to be passed to the new scope of your directive:
Your directive in the html:
<chosen select-recepient-country = "selectRecipientCountry"></chosen>
Your directive code:
uiComponentsModule.directive('chosen', [ function () {
return {
scope: {
selectRecipientCountry: '&selectRecipientCountry',
},
link: function(scope, element, attrs) {
scope.$watch(function (newVal, oldVal) {
scope.selectRecipientCountry(x)
})
}
}
}])
For en explanation, see the article here:
http://weblogs.asp.net/dwahlin/creating-custom-angularjs-directives-part-3-isolate-scope-and-function-parameters
You are looking for:
Option 2: Storing a Function Reference and Invoking It
Edit: We have a good example of passing functions here:
AngularJS - pass function to directive
Also note that:
scope: {
myFunction: '&',
}
and
scope: {
myFunction: '&myFunction',
}
are equivalent. One just explicitly names the attribute in which it will look for the variable and the other assumes the attribute name will be the same as the scope variable name.

AngularJs: Directive with function for template property. How to get scope value?

I'm trying to have different templates for directive. All solutions that I've found were about using $compile service inside linker function.
But we have template property for directive factory that can be a function. Why not to use this option?
So:
return {
template: function(element, attr) {
if(attr.template){
return attr.template;
}
return defaultTemplate;
}
}
and after in html:
<my-directive template="button.template"></my-directive>
Works like charm.
Only one thing is missing: I don't know how to get scope variable button.template inside. Now just getting 'button.template' like a string. Also tried with brackets - same string only with brackets '{{button.template}}'. Is it possible or I should return back to solution with compile service?
The template function is executed before the linking phase. This means that the scope is not yet available when it is evaluated (and this is why it is not injected into the function).
The pattern is to inject $compile into your directive factory and use it in the postLink phase, because at that time, the scope is already available. For example,
app.directive('myDirective', [ '$compile',
function ($compile) {
function postLink(scope, element, attrs) {
// Here, both the DOM and the scope are available, so
// you can extend the DOM with templates compiled against the scope
// if you are using <my-directive template="button.template"></my-directive>
var template = scope.$eval(attrs.template);
// if you are using <my-directive template="{{ button.template }}"></my-directive>
var template = attrs.template; // interpolation is already processed against the scope
// compile the template and append to existing DOM
element.append($compile(template || defaultTemplate)(scope));
}
function template(element, attrs) {
// Here, you cannot evaluate attrs.template against the scope
// because the scope does not yet exist! Only the DOM is
// available (you can use the raw values of attributes if needed)
return '<div></div>';
}
return {
template: template,
link: postLink
}
}
])

AngularJS - ngClick, custom directive, and isolated scope issue

Consider the following directive: (Live Demo)
app.directive('spinner', function() {
return {
restrict: 'A',
scope: {
spinner: '=',
doIt: "&doIt"
},
link: function(scope, element, attrs) {
var spinnerButton = angular.element("<button class='btn disabled'><i class='icon-refresh icon-spin'></i> Doing...</button>");
element.after(spinnerButton);
scope.$watch('spinner', function(showSpinner) {
spinnerButton.toggle(showSpinner);
element.toggle(!showSpinner);
});
}
};
});
which is used like this:
<button ng-click="doIt()" spinner="spinIt">Spin It</button>
When spinner's value (i.e. the value of $scope.spinIt in this example) is true, the element should be hidden and spinnerButton should appear instead. When spinner's value is false, the element should be visible and spinnerButton should be hidden.
The problem here is that doIt() is not in the isolated scope, thus not called on click.
What would be the "Angular way" to implement this directive?
My suggestion is to look at what's going on with these spinners. Be a little more API focused.
Relevant part follows. We use a regular callback to indicate when we're done, so the spinner knows to reset the state of the button.
function SpinDemoCtrl($scope, $timeout, $q) {
$scope.spinIt = false;
$scope.longCycle = function(complete) {
$timeout(function() {
complete();
}, 3000);
};
$scope.shortCycle = function(complete) {
$timeout(function() {
complete();
}, 1000);
};
}
app.directive('spinnerClick', function() {
return {
restrict: 'A',
scope: {
spinnerClick: "=",
},
link: function(scope, element, attrs) {
var spinnerButton = angular.element("<button class='btn disabled'><i class='icon-refresh icon-spin'></i> Doing...</button>").hide();
element.after(spinnerButton);
element.click(function() {
spinnerButton.show();
element.hide();
scope.spinnerClick(function() {
spinnerButton.hide();
element.show();
});
});
}
};
});
Here's one that expects use of $q. It'll work better with Angular-style asynchronous operations, and eliminates the callback functions by instead having the spinner reset on fulfilment of the promise.
Here is the polished version of the directive I ended up with (based on Yuki's suggestion), in case it helps someone: (CoffeeScript)
app.directive 'spinnerClick', ->
restrict: 'A'
link: (scope, element, attrs) ->
originalHTML = element.html()
spinnerHTML = "<i class='icon-refresh icon-spin'></i> #{attrs.spinnerText}"
element.click ->
return if element.is('.disabled')
element.html(spinnerHTML).addClass('disabled')
scope.$apply(attrs.spinnerClick).then ->
element.html(originalHTML).removeClass('disabled')
Use it like so:
<button class="btn btn-primary" spinner-click="createNewTask()"
spinner-text="Creating...">
Create
</button>
Controller's code:
TasksNewCtrl = ($scope, $location, $q, Task) ->
$scope.createNewTask = ->
deferred = $q.defer()
Task.save $scope.task, ->
$location.path "/tasks"
, (error) ->
// Handle errors here and then:
deferred.resolve()
deferred.promise
Yes, it will call doIt in your isolated scope.
You can use $parent.doIt in that case
<button ng-click="$parent.doIt()" spinner="spinIt">Spin It</button>
From the AngularJS Documentation (http://docs.angularjs.org/guide/directive):
& or &attr - provides a way to execute an expression in the context of the parent scope. If no attr name is specified then the attribute name is assumed to be the same as the local name. Given and widget definition of scope: { localFn:'&myAttr' }, then isolate scope property localFn will point to a function wrapper for the count = count + value expression. Often it's desirable to pass data from the isolated scope via an expression and to the parent scope, this can be done by passing a map of local variable names and values into the expression wrapper fn. For example, if the expression is increment(amount) then we can specify the amount value by calling the localFn as localFn({amount: 22}).
so inlclude doIt: "&doIt" in your scope declaration, then you can use doIt as a function in your isolated scope.
I'm confused as to why you are not packaging up everything in the directive as if it's a self-contained module. That's at least what I would do. In other words, you have the click-handler in the HTML, some behavior in the directive and some behavior in the external controller. This makes your code much less portable and more decentralized.
Anyway, you may have reasons for this that are not shared, but my suggestion would be to put all the "Spin It" related stuff in the spinner directive. This means the click-handler, the doIt() function and template stuff all within the link function.
That way there's no need to worry about sharing scope and code entanglement. Or, am I just missing something?
I don't know about the 'angular' way of doing things, but i suggest not using an isolated scope but instead just creating a child scope. You then do attrs.$observe to get any properties you need for your directive.
I.E. :
app.directive('spinner', function() {
return {
restrict: 'A',
scope: true, //Create child scope not isolated scope
link: function(scope, element, attrs) {
var spinnerButton = angular.element("<button class='btn disabled'><i class='icon-refresh icon-spin'></i> Doing...</button>");
element.after(spinnerButton);
//Using attrs.$observe
attrs.$observe('spinner', function(showSpinner) {
spinnerButton.toggle(showSpinner);
element.toggle(!showSpinner);
});
}
};
});
I find this way is better than using '$parent' to escape the isolated scope in other directives (eg ngClick or ngModel) as the end user of your directive does not need to know whether or not using your directive requires them to use '$parent' or not on core angularjs directives.
Using CoffeeScript and a FontAwesome icon.
No need to manually specify the spinner-text
It will just add the spinner content left of the text while loading
We must use finally instead of then for the promise otherwise the spinner will stay there on failure?
I must use $compile because the contents of the button is dynamically compiled as I am using https://github.com/angular-translate/angular-translate
app.directive 'spinnerClick', ["$compile", ($compile) ->
restrict: 'A'
link: (scope, element, attrs) ->
originalHTML = element.html()
spinnerHTML = "<i class='fa fa-refresh fa-spin'></i> "
element.click ->
return if element.is('.disabled')
element.html(spinnerHTML + originalHTML).addClass('disabled')
$compile(element.contents())(scope)
scope.$apply(attrs.spinnerClick).finally ->
element.html(originalHTML).removeClass('disabled')
$compile(element.contents())(scope)
]

how to set an interpolated value in angular directive?

How do I set the interpolated value in a directive? I can read the correct value from the following code, but I have not been able to set it.
js:
app.directive('ngMyDirective', function () {
return function(scope, element, attrs) {
console.log(scope.$eval(attrs.ngMyDirective));
//set the interpolated attrs.ngMyDirective value somehow!!!
}
});
html:
<div ng-my-directive="myscopevalue"></div>
where myscopevalue is a value on my controller's scope.
Whenever a directive does not use an isolate scope and you specify a scope property using an attribute, and you want to change that property's value, I suggest using $parse. (I think the syntax is nicer than $eval's.)
app.directive('ngMyDirective', function ($parse) {
return function(scope, element, attrs) {
var model = $parse(attrs.ngMyDirective);
console.log(model(scope));
model.assign(scope,'Anton');
console.log(model(scope));
}
});
fiddle
$parse works whether or not the attribute contains a dot.
If you want to set a value on the scope but don't know the name of the property (ahead of time), you can use object[property] syntax:
scope[attrs.myNgDirective] = 'newValue';
If the string in the attribute contains a dot (e.g. myObject.myProperty), this won't work; you can use $eval to do an assignment:
// like calling "myscopevalue = 'newValue'"
scope.$eval(attrs.myNgDirective + " = 'newValue'");
[Update: You should really use $parse instead of $eval. See Mark's answer.]
If you're using an isolate scope, you can use the = annotation:
app.directive('ngMyDirective', function () {
return {
scope: {
theValue: '=ngMyDirective'
},
link: function(scope, element, attrs) {
// will automatically change parent scope value
// associated by the variable name given to `attrs.ngMyDirective`
scope.theValue = 'newValue';
}
}
});
You can see an example of this in this Angular/jQuery color picker JSFiddle example, where assigning to scope.color inside the directive automatically updates the variable passed into the directive on the controller's scope.

Angularjs passing object to directive

Angular newbie here. I am trying to figure out what's going wrong while passing objects to directives.
here's my directive:
app.directive('walkmap', function() {
return {
restrict: 'A',
transclude: true,
scope: { walks: '=walkmap' },
template: '<div id="map_canvas"></div>',
link: function(scope, element, attrs)
{
console.log(scope);
console.log(scope.walks);
}
};
});
and this is the template where I call the directive:
<div walkmap="store.walks"></div>
store.walks is an array of objects.
When I run this, scope.walks logs as undefined while scope logs fine as an Scope and even has a walks child with all the data that I am looking for.
I am not sure what I am doing wrong here because this exact method has worked previously for me.
EDIT:
I've created a plunker with all the required code: http://plnkr.co/edit/uJCxrG
As you can see the {{walks}} is available in the scope but I need to access it in the link function where it is still logging as undefined.
Since you are using $resource to obtain your data, the directive's link function is running before the data is available (because the results from $resource are asynchronous), so the first time in the link function scope.walks will be empty/undefined. Since your directive template contains {{}}s, Angular sets up a $watch on walks, so when the $resource populates the data, the $watch triggers and the display updates. This also explains why you see the walks data in the console -- by the time you click the link to expand the scope, the data is populated.
To solve your issue, in your link function $watch to know when the data is available:
scope.$watch('walks', function(walks) {
console.log(scope.walks, walks);
})
In your production code, just guard against it being undefined:
scope.$watch('walks', function(walks) {
if(walks) { ... }
})
Update: If you are using a version of Angular where $resource supports promises, see also #sawe's answer.
you may also use
scope.walks.$promise.then(function(walks) {
if(walks) {
console.log(walks);
}
});
Another solution would be to add ControllerAs to the directive by which you can access the directive's variables.
app.directive('walkmap', function() {
return {
restrict: 'A',
transclude: true,
controllerAs: 'dir',
scope: { walks: '=walkmap' },
template: '<div id="map_canvas"></div>',
link: function(scope, element, attrs)
{
console.log(scope);
console.log(scope.walks);
}
};
});
And then, in your view, pass the variable using the controllerAs variable.
<div walkmap="store.walks" ng-init="dir.store.walks"></div>
Try:
<div walk-map="{{store.walks}}"></div>
angular.module('app').directive('walkMap', function($parse) {
return {
link: function(scope, el, attrs) {
console.log($parse(attrs.walkMap)(scope));
}
}
});
your declared $scope.store is not visible from the controller..you declare it inside a function..so it's only visible in the scope of that function, you need declare this outside:
app.controller('MainCtrl', function($scope, $resource, ClientData) {
$scope.store=[]; // <- declared in the "javascript" controller scope
ClientData.get({}, function(clientData) {
self.original = clientData;
$scope.clientData = new ClientData(self.original);
var storeToGet = "150-001 KT";
angular.forEach(clientData.stores, function(store){
if(store.name == storeToGet ) {
$scope.store = store; //declared here it's only visible inside the forEach
}
});
});
});

Resources