How to delegate ngFocus/ngBlur to directive's template <input> element? - angularjs

I'm trying to create a custom component (directive) which is composed of an <input> box and a [-] and [+] buttons. Currently, the example below only implements the input box.
So, say I have the following HTML for my directive:
<my-input ng-blur="onBlur($event)" ng-focus="onFocus($event)"></my-input>
And for testing purposes, I use this code:
app.run(function ($rootScope) {
$rootScope.onBlur = function ($event) {
console.log('onBlur', $event);
};
$rootScope.onFocus = function ($event) {
console.log('onFocus', $event);
};
});
Now I want to create my custom <my-input> directive which has an <input> box on the template and I need the ng-blur and ng-focus set on <my-input> to respond to blur/focus events on the input box.
I have the following solution almost working: http://codepen.io/anon/pen/KpELmj
1) I have a feeling that this can be achieved in a much better way, I just can't seem to do it. Thoughts?
2) $event seems to be undefined and I can't understand why. Thoughts?

Ok figured it out. Doron's answer was a good starting point for research, but now I think I have what you are looking for. The key is you have to use & in the link section in order to get it to execute the expression.
.directive('myInput', function($timeout) {
return {
restrict: 'E',
scope: {
data: '=',
blur: '&myBlur' //this is the key line
},
template: '<input ng-blur="blur($event)" ng-model="data">'
}
})
This is how you use it:
<my-input my-blur="runBlurFunc()"></my-input>
If you really want to define the function on the root scope, you can use $scope.$root.onBlur() instead of runBlurFunc()

Hope I got your question right, did you try to use the link function?
app.directive('myInput', function () {
return {
restrict: 'E',
scope: {
ngBlur: '&',
ngFocus: '&'
},
bindToController: true,
controller: controllerFn,
controllerAs: 'ctrl',
link:function(scope){
scope.onBlur = function(ev){
console.log(ev);
}
scope.onFocus = function(ev){
console.log(ev);
}
},
template: '[-]<input ng-blur="onBlur($event)" ng-focus="onFocus($event)"></input>[+]'
}
});

Related

When using two way binding in angular, when are the binded variables actually available?

I'm writing a directive that uses two-way binding.My directive looks like this:
bankSearch.directive('bankSearch',
function() {
return {
restrict: 'E',
scope: {
bankDetail: '='
},
templateUrl: "angular/views/self_signup/bank_search.html",
link: function(scope) {
//Now when link function runs, scope.bankDetail is undefined.
}
}
});
Html of template-url:
`<div class="row-fluid">
<input id="ifsc-code" class="span12" type="text" name="ifscCode"
ng-model="bankDetail.bankBranch.ifscCode"
should-be-ifsc
ng-keypress="onPressEnter($event, bankSearch.ifscCode.$valid)">
</div>`
This is how I'm using the directive:
`<bank-search
bank-detail="bankSearchModel.bankDetail">
</bank-search>`
I was under the impression that link function runs after linking(watchers setup) is done. If I'm correct then why am I getting undefined for scope.bankDetail inside my link function.
I'm new to Angular. Thanks for the help!
Might be bankSearchModel.bankDetail defines after directive is rendered. In case it is loaded from $http or something else. Try
link: function(scope) {
scope.$watch('bankDetail', function(val){
console.log(val);
});
}
And you'll see all changes of that variable.

Utility functions for directives

Say I want to make an angular directive that generates links to resources that look like this:
link/to/resource/1234
from an object that looks like:
resource = {
id: 1234,
otherProperty: 'foo'
}
How can I do this effectively w/ a directive? Ie, I'd like to not have to repeat the part that goes '/link/to/resource/{{id}}'. I can't seem to get that to work right. One of the several things I've tried:
app.directive('myResource', function() {
return {
restrict: 'E',
scope: {
resource: '='
},
baseUrl: 'link/to/{{resource.id}}',
template: '{{baseUrl}}'
};
});
which ends up rendering:
Other things I've tried (like making baseUrl a function/sticking it in scope) have resulted in similar things/errors.
Is there a way to get something like this to work?
One way to handle this is to use the directive's link function to set the variable up for you, like this:
link: function(scope) {
scope.baseUrl= 'link/to/'+scope.resource.id;
},
template: '{{baseUrl}}'
Here's a working fiddle
Alternatively you could use this approach:
link: function(scope) {
scope.baseUrl= 'link/to/';
},
template: '{{baseUrl}}{{resource.id}}
Here's the fiddle
just write a directive controller
app.directive('myResource', function() {
return {
restrict: 'E',
scope: {
resource: '='
},
controller: function($scope){
$scope.getBaseUrl = function(resource){
return 'link/to/'+resource.Id;
};
},
template: '<a ng-href="http://{{getBaseUrl(resource)}}">{{getBaseUrl(resource)}}</a>'
};
});
a function makes more sense,because you wont have manage resource state change.You may answer that the Id is unlikely to change but in my opinion,it's better practice in general.
http://plnkr.co/edit/I2QKNB1o8jvZf7kCDT2v?p=preview

Data-binding in a directive template

I'm trying to build a really basic Angular directive that generates a "div" that can have two states: "on" and "off".
I've naively come up with an implementation that you can find here: http://jsfiddle.net/wSz2f/
The initial display is correct but the visual state is not updated when the scope state changes.
Here is the Angular directive definition:
var test = angular.module("test", []);
test.directive("led", function()
{
return {
restrict: "E",
replace: true,
template: "<div class='led led-off' ng-class='{ \"led-on\": isOn }'>{{isOn}}</div>",
link: function(scope, element, attributes, controller)
{
scope.isOn = false;
element.bind("click", function()
{
scope.isOn = !scope.isOn;
});
}
};
});
I guess I'm doing something stupid but what...?
Moreover, in term of design, am doing it the "Angular way" or is there better practices?
Thanks for any input. :)
Final edit:
Thanks to Mark, Bertrand and James for their input:
you have to either call scope.$apply() after updating the "isOn" property to make Angular aware of its change (thinking about it this is how it works too in other frameworks like WPF/Silverlight with the INotifyPropertyChanged interface, but not in Flex with all its magic binding) or use ng-click like suggested by Bertrand
you have to provide "ng-class" a condition for each CSS class
In your case you have to fire the digest
element.bind("click", function()
{
scope.isOn = !scope.isOn;
scope.$apply();
});
or even better, you could use the ng-click directive in your template to change the state of isOn.
var test = angular.module("test", []);
test.directive("led", function()
{
return {
restrict: "E",
replace: true,
template: "<div class='led led-off' ng-class='{ \"led-on\": isOn, \"led-off\": !isOn }' ng-click='onOff()'>{{isOn}}</div>",
link: function(scope, element, attributes, controller)
{
scope.isOn = false;
scope.onOff = function () {
scope.isOn = !scope.isOn;
}
}
};
});
Here is a fiddle
You're code looks good. You are missing the scope.$appy(); after updating the data. The exact internal reasons of why you need to call scope.$apply(); are only partly clear to me. I've read a few articles on it but am no expert yet.
Updated fiddle: http://jsfiddle.net/wSz2f/1/
Reading: http://jimhoskins.com/2012/12/17/angularjs-and-apply.html

Angular Directive Different Template

I have a directive myDirective with variable type. If I run <my-directive type="X"> I want the directive to use templateUrl: x-template.html.
If I do <my-directive type="Y"> I want the directive to use templateUrl: y-template.html.
This is my current directive.
app.directive('myDirective', function() {
var myDirective = {
templateUrl: 'X-template.html',
restrict: 'E',
scope: {
type: '='
},
};
return myDirective;
});
I read thru stackoverflow and angular documentation but have not found anything that I need.
I am now trying to do something along the lines of:
if ($scope.type === 'X') {
templateUrl: 'X-template.html',
}
else if ($scope.type === 'Y') {
templateUrl: 'Y-template.html',
}
But do not know where to do it.
Do you guys know if this is possible and how?
Angular will accept a function as the template option, so you could do something like so:
.directive('myDirective', function () {
return {
templateUrl: function (tElement, tAttrs) {
if (tAttrs) {
if (tAttrs.type === 'X') {
return 'X-template.html';
}
if (tAttrs.type === 'Y') {
return 'Y-template.html';
}
}
}
}
});
For more info, see the documentation for the $compile service.
You can work around this issue using ng-include inside compile:
app.directive('myDirective', function() {
return {
restrict: 'E',
compile: function(element, attrs) {
element.append('<div ng-include="\'' + attrs.type + '-template.html\'"></div>');
}
}
});
fiddle
If you're willing to live on the bleeding edge with a build on the 1.1.x code path (note the warning attached to every 1.1.x build notes entry so I don't dilute this answer by repeating it again here), you're in luck--this very feature was just added in the 1.1.4 release on April 3rd. You can find the release notes for 1.1.4 here and the feature's task log includes a Jasmine test that demonstrates how to use the new functionality.
If you're more conservative and are using a 1.0.x release, then you won't be able to accomplish this as easily, but it can be done. Mark Rajcok's solution looks like it would fit your requirements as-stated, but I would just add a few additional notes:
Aside from its 1.1.4 release, compile-time directives don't support modification at runtime.
As of 1.1.4, you can safely modify the attributes of compile-time directives, but only from another compile-time directive.
You may want to consider replaceWith() instead of append() since <my-directive> is not a standard-defined HTML element type.
If your X and Y templates contain additional directives, I don't think you'll be able to pass attributes on <my-template> through to the root element of your template so easily.
A directive with replace: true will transfer attributes from the source element to its replacement root, but I do not think that ngInclude will do the same from is host to the root of the included template.
I also seem to recall that ngInclude does not require that its template have exactly one root element.
You could perhaps preserve attributes on a replacement parent by using replaceWith() instead of append() and wrapping the <div ng-include=""> tag within a <div></div>. The outer <div> could hold attributes and would still be accessible after the <div ngInclude> element replaced itself with loaded content.
Be aware that ngInclude creates a new scope. This subjects you to a flashing yellow klaxons warning about the dangers of primitive scope models. For more information, see this fine page from Angular's GitHub depot.
I can propose another alternative for those on 1.0.x, but it involves a fair amount of code. It's a more heavy-weight operation, but it has the upside of not only being able of switching between templates, but full-fledged directives as well. Furthermore, its behavior is more readily dynamic.
app.directive('myDirective', function() {
return {
restrict: 'E',
replace: true,
templateUrl: 'partials/directive/my-directive.html',
link: function(scope, element, attrs, ctrl) {
// You can do this with isolated scope as well of course.
scope.type = attrs.type;
}
}
);
my-directive.js
<div ng-switch on="{{type}}">
<div ng-switch-where="X" ng-include="X-template.html"></div>
<div ng-switch-where="Y" ng-include="Y-template.html"></div>
</div>
my-directive.html
This is my version for optionally overriding a default template
templateUrl: function (elem, attrs) {
if (attrs.customTemplate) {
return '/path/to/components/tmpl/' + attrs.customTemplate + '.html';
} else {
return '/path/to/components/tmpl/directive.html';
}
}
e.g on a directive
<div my-directive custom-template="custom"></div>
I solve this problem so:
app.directive("post", function ($templateCache, $compile) {
function getTemplate(mode) {
switch (mode) {
case "create":
return "createPost.html";
case "view":
return "viewPost.html";
case "delete":
return "deletePost.html";
}
}
var defaultMode = "view";
return {
scope: {},
restrict: "AE",
compile: function (elem, attrs, transclude) {
return function ($scope, $element, $attr) {
function updateTemplate() {
$element.html("");
$compile($templateCache.get(getTemplate($scope.mode)).trim())($scope, function (clonedElement, scope) {
clonedElement.appendTo($element);
});
}
$scope.mode = $attr.mode || defaultMode;
$scope.$watch("mode", updateTemplate);
}
}
}
});
It's probably not the best way to do this, but I have no extra scope.
Ok, this might help someone here :-)
To inject your custom attr into your link or controller function use the following.
I'm at work right now but will post a fiddle later if I get a chance :-)
.directive('yourDirective', function() {
return {
restrict: 'EA',
template: '<div></div>', // or use templateUrl with/without function
scope: {
myAttibute: '#myAttr' // adds myAttribute to the scope
},
link: function(scope) {
console.log(scope.myAttibute);
}
}
}
// HTML ""
// Console will output "foo"

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