I have a directive that abstracts a menu. The menu item selection needs to be notified to its parent controller so it can take required action. There are multiple ways I could get this achieved.
Pass a scope variable from controller to directive and observe the change on this variable. Within the directive change this variable to indicate the selection option.
Pass a callback method from controller to directive. Invoke the callback from directive upon change.
Observe the changes in controller using $scope.$on and notify from the directive using scope.$emit
I could not clearly arrive at which one option is better. I am leaning towards option 3 as it seems to be cleaner but I am not sure if this has a unwanted coupling. I would like to hear an opinion from others, which solution would favour clear dependency and good for testability.
UPDATE:
After reading the suggestions and thought, I picked up Option 2 for below reasons:
It is very obvious by looking into the HTML about the dependency
<menu save="onSave()" filterByDate="filterByDate(date)"></menu>
Unit testing is very explicit and tell about the API (interface) the directive
I personally don't like using observe\emit\on unless i have too. It isn't always obvious looking at someone elses code where it is being set. This can lead to spaghetti code. If you have a call back, i find the direct link is more obvious to the eye and easier to find. This is in conjunction with well named properties etc. At the end of the day it's a matter of personal\team taste.
I recommend using a modified version of 1. But instead of passing a variable, inject a service wherever you need.
The service can look something like this:
yourApp.factory('SelectedMenu',
function () {
//set a default or just initialize it
var menuItem = {
id: 1,
code:"x"
};
return {
getId: function () { return menuItem.id; },
getCode: function() { return menuItem.code;},
setId: function(newId){menuItem.id = newId},
setCode: function(newCode){menuItem.code=newCode;},
};
}
);
In the directive(s) which controlls the behavior create a watch:
$scope.$watch(
function() {
//should be idempotent! can execute multiple times per $digest cycle when a change is detected
// (good compromise for not polluting $rootScope)
return SelectedMenu.getId();
},
function( newValue) {
//do your thing...
}
);
When you need to use it/update the value you can set the code or any other property you want and finally set the id to trigger the update.
SelectedMenu.setId(someones.id);
It is the cleanest solution i came across. If you want, you can abstract the usage even more with another service if the data object is more complex.
Related
As digest cycle do the dirty checking of the variable that is if there are 100 scope variables and if I change one variable then it will run watch of all the variables.
Suppose I have 100 scope model variables that are independent of each other. If I make changes in one variable then I don't want to check all other 99 variables. Is there any way to do this ? If yes, how ?
Surprisingly, this is usually not a problem, Browsers don’t have problems even with thousands of bindings, unless the expressions are complex. The common answer for how many watchers are ok to have is 2000.
Solutions :
It is fairly easy onwards from AngularJS 1.3, since one-time bindings are in core now.
One time Binding of the variables.
We can use One time binding(::) directive to prevent the watcher to watch the unwanted variables. Here, variable will be watch only once & after that it will not update that variable.
Stop the digest cycle manually.
HTML :
<ul ng-controller="myCtrl">
<li ng-repeat="item in Lists">{{lots of bindings}}</li>
</ul>
Controller Code :
app.controller('myCtrl', function ($scope, $element) {
$element.on('scroll', function () {
$scope.Lists = getVisibleElements();
$scope.$digest();
});
});
During the $digest, you are only interested in changes to Lists object, not changes to individual items. Yet, Angular will still interrogate every single watcher for changes.
directive for stop and pause the digest:
app.directive('stopDigest', function () {
return {
link: function (scope) {
var watchers;
scope.$on('stop', function () {
watchers = scope.$$watchers;
scope.$$watchers = [];
});
scope.$on('resume', function () {
if (watchers)
scope.$$watchers = watchers;
});
}
};
});
Now, Controller code should be changed :
<ul ng-controller="listCtrl">
<li stop-digest ng-repeat="item in visibleList">{{lots of bindings}}</li>
</ul>
app.controller('myCtrl', function ($scope, $element) {
$element.on('scroll', function () {
$scope.visibleList = getVisibleElements();
$scope.$broadcast('stop');
$scope.$digest();
$scope.$broadcast('resume');
});
});
Reference Doc : https://coderwall.com/p/d_aisq/speeding-up-angularjs-s-digest-loop
Thanks.
This is a good question and highlights one of the biggest deficiencies with Angular 1.x. There is little control over how the digest cycle is managed. It is meant to be a black box and for larger applications, this can cause significant performance issues. There is no angular way of doing what you suggest, but There is something that would help you achieve the same goals (ie- better performance of the digest cycle when only one thing changes).
I recommend using the bind-notifier plugin. I have no relationship with the project, but I am using it for my own project and have had great success with it.
The idea behind is that you can specify certain bindings to only be $digested when a specific event has been raised.
There are multiple ways of using the plugin, but here is the one that I find must effective:
In a template file, specify a binding using the special bind-notifier syntax:
<div>{{:user-data-change:user.name}}</div>
<div>{{:job-data-change:job.name}}</div>
These two bindings will not be dirty-checked on most digest cycles unless they are notified.
In your controller, when user data changes, notify the bindings like this:
this.refreshUserData().then(() => {
$scope.$broadcast('$$rebind::user-data-change');
});
(and similar for job-data-changed)
With this, the bindings for user.name will only be checked on the broadcast.
A few things to keep in mind:
This essentially subverts one of the key benefits of angular (also it's core weakness for large applications). Two way binding usually means that you don't need to actively manage changes to your model, but with this, you do. So, I would only recommend using this for the parts of your application that have lots of bindings and cause slowdowns.
$emit and $broadcast themselves can affect performance, so try to only call them on small parts of the $scope tree (scopes with few or no children).
Take a good look at the documentation since there are several ways to use the plugin. Choose the usage pattern that works best for your application.
This is quite a specific use-case to do exclusive/conditional checks in the digest cycle and I don't think it is possible without forking/hacking the angular core.
I would consider refactoring how/what you are $watching. Perhaps using ngModelController's $viewChangeListeners would be more suitable than $watch?
I want to expose some of my directive's functionality through its controller (think a public API for this directive).
return {
restrict: 'E',
scope: {},
controller: function($scope) {
this.method1 = ...;
this.method2 = ...;
},
controllerAs: 'dir',
link: function (scope, element, attrs) { ... }
}
Then in my parent controller or template call dir.method1 to get stuff accomplished inside the directive. Any ideas if this is possible as of Angular 1.3?
I'd like to refrain from event passing or even function passing, I have heard this is possible although I have never seen an implementation of this.
It is possible, but your issue isn't to figure out how to get the API out. It's how to get TO it from the parent. You're creating an isolate scope through your use of the 'scope' option. You're also making an element-type directive, so I'm guessing you're doing something like this:
<my-parent>
<my-child></my-child>
</my-parent>
where <my-parent> is the parent directive, and <my-child> is the directive with the API you want to expose.
The real question is what you're trying to achieve here. There is totally a way to do what you're asking. Just because the scope is isolated doesn't mean you can't get to it. You can just iterate through the parent $scope's $$childHead/etc list to find the child whose API you want to access. Anything you define in the child like this:
$scope.myApiFunction = function() {
};
will be visible here. (Things you put into 'this' will not - use the $scope storage bucket instead.)
That means if you only had ONE child you could do something like this from the parent controller:
$scope.$$childHead.myApiFunction();
Simple. Also, very crude. There are lots of problems here: what if you have many children? What if this child with its API ends up one level down? Etc. It's breaking all kinds of OO patterns and it's going to get messy, fast.
Your question is very abstract - it might be good if you updated it with an exact example. Without that, let me guess at your goal. There are two ways to do something "like this" that are encouraged within Angular:
Services. Whenever you say "API", think Service first. A service is a singleton (automatically) so it's tailor-made for creating APIs. And services can use the Factory pattern to return objects of a type, so THOSE are tailor made for doing things like having a manager service handle, say, a buddy list in an IM client, with API methods for creating, removing, and finding buddies.
Items that add "optional" functionality to their parents when they're defined. Let's say we have three possible types of tooltips: tooltips that have a hover effect, those that have a click effect, and those that are triggered by a "walkthrough" system in some order. For this kind of thing, the easy thing to do is just reverse the API, like this:
Parent Controller:
$scope.tooltipHandler = {
showTooltip: function() {},
hideTooltip: function() {}
};
Child Controller:
$scope.$parent.tooltipHandler = {
showTooltip: function() {
// Do some real work
},
hideTooltip: function() {
// Do some real work
},
}
What happens here is if there's no tooltip defined, when the parent runs its walkthrough, nothing happens. If you add the blue tooltip display module, when the parent runs its walkthrough now, it's going to show blue tooltips.
Make sense?
I arrived here looking for a similar response. So far the best that I can figure is to do what Angular does with ngForm.
In the documentation clearly states
If the name attribute is specified, the form controller is published onto the current scope under this name.
This basically makes the form controller accessible from anywhere.
If you have the following DOM
<div ng-controller="MyCtrl as parentCtrl">
<form name="parentCtrl.frmCtrl">
<my-child-directive>
</form>
</div>
You can use require: 'ngForm' in my-child-directive to get access from an inside directive. If you are in the parent controller you can access it trough the frmCtrl variable.
Not sure if this is best practice. In ngForm the name attribute works well, but I don't even know how to call such an attribute for a custom directive.
Thats why I arrived here, I wanted to know if this is "The Angular way" and what types of convetions are on the subject.
Hope it helps!
In angularJs is possible to watch a global variable?
I set a window.test variable from legacy code, then I need to watch that variable to know if it exists.
I've tried something like
$window.$watch("test" , function(n,o){
//some code here...
}
Somewhat. You can if you include the Angular $window service (which is safer, as explained in the docs, than accessing window directly):
app.controller('myCtrl', function ($scope,$window) {...}
And then use a watch function as the first parameter to your $watch like so:
$scope.$watch(
function () {
return $window.test
}, function(n,o){
console.log("changed ",n);
}
);
demo fiddle
But note that the $watch won't execute until something triggers Angular to do a $digest. One possible way to do that is to wrap your legacy code in a $scope.$apply or trigger a $digest once the legacy code has exectuted. Here's some good documentation on this.
Basically whenever a change happens outside of angular (for instance this is a common issue when jQuery causes the change) something has to tell Angular to go see if something changed. It's one way Angular maintains reasonable performance.
I would like to subscribe to an ngChange event, but from code rather than the markup. That is, given a $scope and an expression that is bindable via ngModel, I want to subscribe to any changes made to that expression by any ngModel directive that binds to that expression. Is this possible?
something like:
$scope.field = "hello";
$scope.onButtonClick = function() {
$scope.field = "button clicked!";
}
// this callback is only when the user types in an input bound to field
// not when they click the button with ng-click="onButtonClick()"
$scope.$watchNgChange("field", function() {
console.log("user caused field to change via a binding");
});
// this callback is called for both ngModel binding changes and onButtonClick.
$scope.$watch("field", function() {
console.log("field was changed");
});
I can't just use $watch, because that will capture all changes, including those from loading the data from the database, from ng-click callbacks, and changes initiated from $watch callbacks for other expressions (in this case, if there are any circular references, then it's too easy to have $watch callbacks to get into an infinite loop and error out after 10 digest cycles), and who knows what else.
First, anytime I have tried to do something like this is turned out to be a bad idea - I was just working around design problems in my code or logic problems in my business logic. In general, the code should not care HOW the data was changed, only that it HAS changed.
Second, $watch can give you both the old and new value - this has been enough for me. if the old value not equal to the new value, I want to update the related data model(s). If that old and new value are equal, I want to ignore the update.
Finally, You may consider using resolve with your routes eliminate "database loading" as the fully located data can be passed into your controller (assuming you return a promise).
.
Jeremy you just describe what in Angular is known as a Directive. It is best practice to always use directives each time you need to touch the DOM. This logic should never live in the controller or even the Service.
directives are a big tricky but there is tons of documentation for it.
Visit docs.angularjs.org/directives
Don't do it, it sounds like you are trying to introduce the DOM logic (e.g. if the user has interacted with a DOM element) into the controller.
If you read the source code of a ngChange directive, you will found it requires a ngModel which is used as the bridge between the view and the controller.
I recommend creating a copy of the model and used the copy for data binding using ngModel+ngChange in your view, and then you can $watch that copy and do whatever you want.
$scope.field = "hello"; //the field you care
$scope.fieldCopy = $scope.field; //use 'fieldCopy' for databinding
In the html code you can have multiple way of changing the model fieldCopy
<input ngModel="fieldCopy" name='foo' />
<input ngModel="fieldCopy" name='foo2' />
You then watch the fieldCopy for changes related to user interaction and copy the change to 'field':
$scope.$watch("fieldCopy", function() {
$scope.field = $scope.fieldCopy;
console.log("user caused field to change via a binding");
});
If you want to keep fieldCopy in sync with field, add another watch:
$scope.$watch("field", function() {
$scope.fieldCopy = $scope.field;
});
This is an angularjs app. I have a service that handles the loading of content (ajax). While the service is getting the content, a number of things throughout the app hide, later showing again (depending on the content returned). They might have the same scope, different scope, whatever. They just need to hide while content is loading, and then show when it's done. Pretty normal stuff.
Right now, I have separate controllers watching a "loading" property of the service and using regular angular directives (ng-show, ng-hide, etc.) to show/hide. But this feels like overkill. I'd prefer to write a custom "loading" directive that injects the loading service and does the watching and showing/hiding.
My question is: Is what I want to do "bad"? The controller way, I end up boilerplating a bunch of code, maybe up to like 5 or 6 times, or even more as the app grows. The custom directive way, I write it once and use an attribute where I need it. Yeah - there's a dependency on that service, but that just doesn't feel like the end of the world that some people have made me start to think I should think it is.
For what it's worth, I feel like I've heard "separation of concerns" so many times I've become paralyzed by it. It leads me to overthink everything because I want to do things the right way, but it sure doesn't feel like I'm being very productive.
If I understood correctly, you have a bunch elements that should hidden when a particular service is loading data, and then be displayed again when the data is loaded, right?
In that case, events might be a good solution:
they can be global to the appliciation (which i think is what you are aksing for).
they allow for avoiding direct coupling between elements (also one of your concerns).
So, in your service, just broadcast events when stuff happens:
$rootScope.$broadcast('loading-data');
axajStuffOrWhatever(function() {
$rootScope.$broadcast('data-loaded');
});
Then, wrap the show/hide behaviour in a directive that will listen to those events.
.directive('hideWhileLoadingData', function() {
return {
link: function(scope, el, attrs) {
scope.$on('loading-data', function() {
el.css('display', 'none');
});
scope.$on('data-ready', function() {
el.css('display', 'block');
});
}
};
});
Use the directive:
<div hide-while-loading-data>something</div>
The advantage of using events here, is that later on, they could be originated by a different service, or by multiple services, and the directive will not be affected by that as long as the events are the same.
For more complex behaviour, you could also parametrize the events and the directive, so different elements will react to different kind of stuff.
I've made an example of this.
In my opinion all scopes which depend on this service should be children of one parent scope. If you have the parent scope responsible for talking with the service then any directive of any scope can access it via $parent on the $scope.