angularjs directive use parameter inside link function - javascript

This is probably a stupid question, but I have been stuck on it for days and neither of the googled solutions panned out for me.
I'm writing an angular 1.4 app following a directive driven approach, so for any entity I have one or more widgets:
isolated scope
controller as
bindToController true
The problem is of the "user has stuff" variety.
<user-widget>
<stuff-widget userid="user._id"></stuff-widget>
</user-widget>
The userid passes nicely into the widget, but I would like to use it inside the stuff widget's link function, because stuff is really complicated and in the real world I need to grab various other parts as well.
I tried various methods to get to the userid value (as suggested in various other stackoverflow discussions and elsewhere on the web)
use bidrectional binding -> not effective
tried via scope and controller -> userid, uid not defined
require ^userWidget -> lost access to my controller
use attrs.$observe('userid', ....) -> js error: userid not defined
passed it via pre link of the parent -> did work, but not a good idea/limited merits
I have a plunker with the various things I tried: http://plnkr.co/edit/SqlhYSteCDxMaAVZWCsy?p=info
The widgets look like this (working pre link variant)
function userWidget() {
return {
restrict: 'EA',
scope:{},
template: '<h2>User</h2> {{user.name}}<stuff-widget userid="user._id"></stuff-widget>',
replace: false,
controllerAs: 'userCtrl',
bindToController: true,
controller: 'UserCtrl',
link: { // this actually works, not sure wether this a good idea
pre: function preLink(scope, element, attrs, ctrl) {
var uid = 1;
scope.user = ctrl.findById(uid);
scope.userid = scope.user._id;
},
post: function postLink(scope, element, attrs, ctrl) {}
}
};
}
function stuffWidget() {
return {
restrict: 'EA',
scope: {
uid: '=userid'
},
template: '<h3>User stuff</h3>stuffCtrl.userid inside widget: {{stuffCtrl.userid}}<div ng-repeat="stuff in stuffCtrl.userStuff">\
User id: {{stuff.userid}}: {{stuff.stuff}}\
</div>',
replace: false,
controller: 'StuffCtrl',
controllerAs: 'stuffCtrl',
bindToController: {
userid: '='
},
link: function (scope, element, attrs, ctrl) {
console.log('stuff ctrl userid:', ctrl.userid); // not defined
console.log('stuff scope uid:', scope.uid); // not defined
console.log('Scope parent userid: ',scope.$parent.userid); // undefined
// didn't work either - userid not defined
/* attrs.$observe('userid', function(value) {
if (value) {
ctrl.userid = userid;
console.log('stuff ctrl userid observed:', ctrl.userid);
}
}); */
ctrl.userStuff = ctrl.findByUserId(ctrl.userid);
}
};
}
I'm an angular beginner (first serious project) and so far my experience has been: "if it's hard to figure out, you are probably doing it the wrong way".
So,
Did I miss an obvious way to access the param inside the link function?
Did I screw up the ways I tried (especially $observe) because I incorrectly transferred them to my setting?
Should I just stick the controller calls inside the template and be done with it?
Should I go about it in an entirely different way? compile function? controller function? whatever other angular depths I'm not yet familiar with?

After the link function has finished its execution you will have to wait one digest cycle to update the values in the isolated scope from the scope where the directive is used.
You can try this snippet in stuffWidged:
link: function(scope, element, attrs, ctrl) {
var uid, usrStuff;
scope.uid; // it is normal to be undefined here
scope.$watch('uid', function(newUid) {
uid = newUid;
scope.uid; // is ready and is same as newUid
usrStuff = ctrl.usrStuff = scope.usrStuff =
ctrl.findById(uid);
});
}

Remove isolated scope from user-widget - do u really need it there? If you are not defining any additional variables in directive scope - you do not actually need isolated scope.
Isolated scope. So user-widget should have some parameters? Like:
<user-widget widgetusers="model.users"></user-widget>
then user-widget template will be:
<stuff-widget ng-repeat="stuffuser in widgetusers" userid="stuffuser._id"></stuff-widget>
You can pass userId first to userWidget, then to stuffWidget:
<user-widget userid="user._id">
<stuff-widget userid="userid"></stuff-widget>
</user-widget>

Related

AngularJS: bind to parent ngChange [duplicate]

//main controller
angular.module('myApp')
.controller('mainCtrl', function ($scope){
$scope.loadResults = function (){
console.log($scope.searchFilter);
};
});
// directive
angular.module('myApp')
.directive('customSearch', function () {
return {
scope: {
searchModel: '=ngModel',
searchChange: '&ngChange',
},
require: 'ngModel',
template: '<input type="text" ng-model="searchModel" ng-change="searchChange()"/>',
restrict: 'E'
};
});
// html
<custom-search ng-model="searchFilter" ng-change="loadResults()"></custom-search>
Here is a simplified directive to illustrate. When I type into the input, I expect the console.log in loadResults to log out exactly what I have already typed. It actually logs one character behind because loadResults is running just before the searchFilter var in the main controller is receiving the new value from the directive. Logging inside the directive however, everything works as expected. Why is this happening?
My Solution
After getting an understanding of what was happening with ngChange in my simple example, I realized my actual problem was complicated a bit more by the fact that the ngModel I am actually passing in is an object, whose properties i am changing, and also that I am using form validation with this directive as one of the inputs. I found that using $timeout and $eval inside the directive solved all of my problems:
//main controller
angular.module('myApp')
.controller('mainCtrl', function ($scope){
$scope.loadResults = function (){
console.log($scope.searchFilter);
};
});
// directive
angular.module('myApp')
.directive('customSearch', function ($timeout) {
return {
scope: {
searchModel: '=ngModel'
},
require: 'ngModel',
template: '<input type="text" ng-model="searchModel.subProp" ng-change="valueChange()"/>',
restrict: 'E',
link: function ($scope, $element, $attrs, ngModel)
{
$scope.valueChange = function()
{
$timeout(function()
{
if ($attrs.ngChange) $scope.$parent.$eval($attrs.ngChange);
}, 0);
};
}
};
});
// html
<custom-search ng-model="searchFilter" ng-change="loadResults()"></custom-search>
The reason for the behavior, as rightly pointed out in another answer, is because the two-way binding hasn't had a chance to change the outer searchFilter by the time searchChange(), and consequently, loadResults() was invoked.
The solution, however, is very hacky for two reasons.
One, the caller (the user of the directive), should not need to know about these workarounds with $timeout. If nothing else, the $timeout should have been done in the directive rather than in the View controller.
And two - a mistake also made by the OP - is that using ng-model comes with other "expectations" by users of such directives. Having ng-model means that other directives, like validators, parsers, formatters and view-change-listeners (like ng-change) could be used alongside it. To support it properly, one needs to require: "ngModel", rather than bind to its expression via scope: {}. Otherwise, things would not work as expected.
Here's how it's done - for another example, see the official documentation for creating a custom input control.
scope: true, // could also be {}, but I would avoid scope: false here
template: '<input ng-model="innerModel" ng-change="onChange()">',
require: "ngModel",
link: function(scope, element, attrs, ctrls){
var ngModel = ctrls; // ngModelController
// from model -> view
ngModel.$render = function(){
scope.innerModel = ngModel.$viewValue;
}
// from view -> model
scope.onChange = function(){
ngModel.$setViewValue(scope.innerModel);
}
}
Then, ng-change just automatically works, and so do other directives that support ngModel, like ng-required.
You answered your own question in the title! '=' is watched while '&' is not
Somewhere outside angular:
input view value changes
next digest cycle:
ng-model value changes and fires ng-change()
ng-change adds a $viewChangeListener and is called this same cycle.
See:
ngModel.js#L714 and ngChange.js implementation.
At that time $scope.searchFilter hasn't been updated. Console.log's old value
next digest cycle:
searchFilter is updated by data binding.
UPDATE: Only as a POC that you need 1 extra cycle for the value to propagate you can do the following. See the other anwser (#NewDev for a cleaner approach).
.controller('mainCtrl', function ($scope, $timeout){
$scope.loadResults = function (){
$timeout(function(){
console.log($scope.searchFilter);
});
};
});

In AngularJS, how do you make a directive an object, so that you can "pass messages" to it (invoke method on it)?

I am quite familiar with CanJS, and kind of like the idea that you can instantiate a custom web widget on an HTML element, and now that we have an object, we can send messages to it (invoke a method on it):
lightbox.popUp();
or
reviewStars.setStars(3.5);
How could that be done in AngularJS? After you make a directive and set it on an HTML element or use the directive as an HTML element, how do you do something like above, as in OOP, or how Smalltalk would do it -- sending messages to a particular object?
I could think of a way, such as:
<review-stars api="ctrl.reviewStarAPI"></review-stars>
and then for the reviewStar directive, do this:
scope: { api: "=" }
link: function(scope, elem, attrs) {
// first define some functions
scope.setStars = function(n) { ... };
// and then set it to the api object:
scope.api.setStars = scope.setStars();
}
and then in the controller, do
vm.reviewStarAPI.setStars(3.5);
but this is a bit messy, and somewhat ad hoc. And it always need to have a controller... although, I suppose we can use 1 controller and as the main program and instantiate a bunch of objects and then invoke methods on them this way.
What is/are ways to accomplish this besides the method above?
A modular approach to this would be to create a directive called reviewStars. The directive should have a parameter that indicates the star rating.
For example:
<review-stars rating="3.5">
You would create using something like the following:
angular.module('myAngularModule', [])
.directive('reviewStars', function() {
return {
restrict: 'E',
scope: {},
bindToController: {
rating: '#'
},
link: function(scope, elem, attrs) {
// instantiate the lightbox code here
},
controller: function () {
var vm = this;
// controller code goes here (e.g. the onClick event handler)
},
controllerAs: 'ctrl',
templateUrl: 'review-stars.html' // the HTML code goes in this file
};
});
Check out Rangle's ng-course (https://github.com/rangle/ngcourse) or the Angular docs (docs.angularjs.org) for more on directives.

$watch doesn't work if I specify a scope outside the link function

I just started working on an Angular app that uses flot to plot a bunch of data. It worked fine for static data, but once we got the directive wired up to mongo, I had to follow the tutorial here to get it working for updating data. I had a hell of a time for one specific reason:
This is my directive HTML:
<div class="panel-body" data-ng-controller="flotChartCtrl">
<div data-flot-line-chart data-data="revenueData.data" data-options="line1.options" style="width: 100%; height: 300px;"></div>
</div>
and javascript:
.directive("flotLineChart", [
function () {
return{
restrict: 'A',
scope: {
data: "=",
options: "="
},
link: function(scope, elem, attrs){
var chart = null;
// var options = { ... };
scope.$watch('data', function(data, oldData) {
if(!chart) {
chart = $.plot(elem, data, options);
elem.show();
} else {
chart.setData(data);
chart.setupGrid();
chart.draw();
}
});
}
};
}
])
As you can see in the html, I'm using the data-options attribute to pass the line1.options object into the directive. When I was just using static data and not using ng-model or the $watch function, this worked and the scope: { options: "=" } assignments were correct. However it seems that whenever I set anything on the scope outside link, it breaks the $watch. $watch always receives a data of undefined... and my scope.options are also undefined. Outside of the $watch function scope.options is correct, but that doesn't help me much if I can't use them when the data is actually plotted.
I've had to resort to hard coding the options inside link: and commenting out the outer scope assignments. I have a bunch of different charts I need to create, all of which look differently. I'd hate to have to hard code different options for EVERY one, but at the moment I don't see any other way to make this work. Is there some way I can access my other data attributes from the HTML inside the $watch function without it breaking everything?
Note: I tried attrs.options, but that just gives me a "line1.options" string, and not the actual object.
Edit1:
Updated my code per ExpertSystem's suggestions. No longer using ng-model.
scope is still not available inside $watch:
Your directive should look like this:
...
restrict: 'A',
scope: {
data: '=ngModel',
options: '='
},
link: function(scope, elem, attrs){
...
scope.$watch('data', function(newValue, oldValue) {
...
Althouth the use of ngModel seems redundant here.
This fiddle demonstrates that scope is indeed defined in the $watch callback.

Isolate Scope "=" binding and doted notation AngularJS

How do you create a 2 way binding with a nested property in an isolate scope with dotted notation. I thought 'myObject.data': "=data" would work, but it does not. I don't want to link everything in the myObject object. I know I could do some sort of watch, but 'myObject.data' seems cleaner.
.directive("myDirective", [function() {
return {
restrict: "E",
scope: {
'myObject.data': "=data"
},
link: function (scope, element, attrs) {
scope.myObject = {
data: "myValue"
};
}
};
}])
Isolated scopes are generally useful only with templates, they should not be used as a way to declare how you want your directive attributes to be interpreted. This is because most directives that don't have a template usually need the semantics of either a child scope or the direct scope of their environment.
In your case, you probably don't even need a $watch, because object references are what enable 2 way data binding, but without your full code I cannot be sure.
In case you want to know the translations for an isolated scope semantics to just a normal one:
#name -> attrs.name
=name -> $scope.$eval(attrs.name);
&name -> function() { return $scope.$eval(attrs.name); }
EDIT 2:
After your comment, I came up with this plunker. To preserve two way data binding you have to use a "." in your ng-model declaration. This is because two way data binding does not work for value types, since they are immutable. You can't change the value of 100 for example. You need to pass around a reference type object and hang the values you are changing off of it. Your desire to specify the full path to the value in the isolated scope definition is not possible based on the principles that two way data binding is made possible by.
Javascript:
angular.module('plunker', [])
.directive('twoWay', function() {
return {
restrict: 'E',
template: '<div><input ng-model="thing.name" type="text" /></div>',
scope: {
thing: "="
},
link: function(scope, element, attrs) {
}
};
})
.controller('MainCtrl', function($scope) {
$scope.data = {
name: "World"
};
});
HTML:
<body ng-controller="MainCtrl">
<p>Hello {{data.name}}!</p>
<two-way thing="data"></two-way>
</body>
What I use in these cases is the following:
.directive("myDirective", [function() {
return {
restrict: "E",
scope: {
data: "="
},
controller: function($scope){
$scope.dot = $scope //<--- here is the trick
}
};
}])
Then you can always change data in the directive's scope from an inherited scope through dot.data = 'whatever' without setting watchers.
Not very elegant but it works jsut fine in cases where you are not using the controller as syntax and don't want a $parent nightmare.

Scoping issue when setting ngModel from a directive

I have a directive which looks something like:
var myApp = angular.module('myApp',[])
.directive("test", function() {
return {
template: '<button ng-click="setValue()">Set value</button>',
require: 'ngModel',
link: function(scope, iElement, iAttrs, ngModel) {
scope.setValue = function(){
ngModel.$setViewValue(iAttrs.setTo);
}
}
};
});
The problem is that if I use this directive multiple times in a page then setValue only gets called on the last declared directive. The obvious solution is to isolate the scope using scope: {} but then the ngModel isn't accessible outside the directive.
​
Here is a JSFiddle of my code: http://jsfiddle.net/kMybm/3/
For this scenario ngModel probably isn't the right solution. That's mostly for binding values to forms to doing things like marking them dirty and validation...
Here you could just use a two way binding from an isolated scope, like so:
app.directive('test', function() {
return {
restrict: 'E',
scope: {
target: '=target',
setTo: '#setTo'
},
template: '<button ng-click="setValue()">Set value</button>',
controller: function($scope) {
$scope.setValue = function() {
$scope.target = $scope.setTo;
};
//HACK: to get rid of strange behavior mentioned in comments
$scope.$watch('target',function(){});
}
};
});
All you need to do is add scope: true to your directive hash. That makes a new inheriting child scope for each instance of your directive, instead of continually overwriting "setValue" on whatever scope is already in play.
And you're right about isolate scope. My advice to newbies is just don't use it ever.
Response to comment:
I understand the question better now. When you set a value via an expression, it sets it in the most immediate scope. So what people typically do with Angular is they read and mutate values instead of overwriting values. This entails containing things in some structure like an Object or Array.
See updated fiddle:
http://jsfiddle.net/kMybm/20/
("foo" would normally go in a controller hooked up via ngController.)
Another option, if you really want to do it "scopeless", is to not use ng-click and just handle click yourself.
http://jsfiddle.net/WnU6z/8/

Categories

Resources