AngularJS: Function in controller called multiple times by template - javascript

I know this question has been asked in here before it seem that all the answers is either quotes from AngularJS doc or doesn't provide with a solution (not a solution I understand anyway) so I'll give it a try once more.
My experience with Angular is relatively new, started out some month ago, so please forgive my ignorance if this is basic knowledge.
Within a list of posts (iterate by using ng-repeat) I've a special "share to" button.
The link on the button (href) depends on three different factors: post_id, user_id, network.
I'm trying to do this, within my ng-repeat="post in posts"
<a href ng-href="{{genShareUrl(post.id,post.author_id,'fb')}}>Facebook</a>
The original function which perform the generation is in a factory, I just use genShareUrl as a middleman function between controller and factory.
When logging out from the genShareUrl function in the post controller, I see this function is called multiple times.
Actually, if I run it on full scale on all posts fetched from backend, my app just come to a halt. No error, just eternal loading (I figured that I might have inadvertently triggered some kind of eternally $digest loop I'm unfamiliar with or at least some exponentially call pattern).
I've tried to recreate the scenario with a simple example:
http://codepen.io/Mestika/pen/xVexRa
Here I can see, that the function first is called twice, then four times - which indicates to me that the digest cycle is triggered multiple time.
In such a case as described, how would I best go about generating some value in a link? Is this the best practice? If no, how or could you give me an example on how to refactor the code.

Angular uses dirty checking to achieve two-way binding, all two-way binding watchers would be evaluated in each digest cycle, that is the reason genShareUrl be called multiple times, to avoid this happened, you could use one-way binding in your template:
<a href ng-href="{{::genShareUrl(post.id,post.author_id,'fb')}}>Facebook</a>

I'd like suggest a best practice for your case:
Resolve start-up logic for a controller in an activate function.
So, you can add an activate function as follows:
activate();
function activate() {
angular.forEach($scope.json, function (p) {
p.btoa = $scope.func(p.id);
});
}
After that, you can update your template and use:
<div ng-repeat="person in json">
<a href ng-href="/user/{{person.btoa}}">{{person.firstName + ' ' + person.lastName}}</a>
</div>
In that way you'll avoid multiple calls to your function due to two-way data binding behavior.
Take a look following links:
Angular Styleguide
Your example updated: http://codepen.io/anon/pen/wGZBEX

Related

How do I change one Directive when I click another Directive?

So here's my problem:
I have a page which displays two different graphs. Each of these graphs are there own Directives which has their own isolate scope.
When a user clicks on one of the bar's in the chart in Directive #1, I need the graph in Directive #2 to change.
Currently both Chart Directives are being fed their respective data sets from the Controller of this page.
Now from what I've seen I really have about three options:
Pass a callback function into Directive #1 which be called when the chart is selected. This callback function will exist on the Controller of the page and then can change the necessary data in order to get Directive #2 to update via data-binding.
Events. Fire an event on $rootScope inside of Directive #1 when the chart is selected. I can then listen to this event on the Controller and change the data in Directive #2 to update it via data-binding.
Use a Library like Rx.JS in order to make an observable inside of Directive #1. I haven't used Rx.JS with Angular that much so to be honest I have no idea if this would even work or what it would look like. But if I could expose this Observable to page's Controller from within Directive #1 then I should be able to subscribe to it and update Directive #2 when necessary.
Now I have a good understanding of Solution #1 and #2 but they have their own issues:
This very quickly could turn into "callback hell" and doesn't seem to be a very "Angular" solution. This also creates a bit of a tight dependency between the page's Controller and this very generic Chart Directive. Out of my options I think this is the best solution but I would love a better one.
I have to build a way to specify id's on the event names that are unique to that explicit instantiation of the directive, since theoretically there could be more than one of these Chart Directives on the page.
I would love to know if anyone has any other ideas that I haven't thought of or a better approach? Maybe even something that I'm not aware of that Rx.JS offers with Observable's?
TLDR: I need to click on Directive #1 and have it effect what is currently being displayed in Directive #2.
I think this can be done by using two binding scopes in your directive like,
.directive('graphOne', function () {
return {
template: blah/blah.html,
scope: {
scopeToPass: '='
}
}
})
and
.directive('graphTwo', function () {
return {
template: blah1/blah1.html,
scope: {
scopeToGet: '='
}
}
})
and in html
<graph-one scope-to-pass="uniqueScope"></graph-one>
<graph-two scope-to-get="uniqueScope"></graph-two>
Since we are assign $scope.uniqueScope to both directives, and the scopeToPass is two way binding, when the value of scopeToPass get changed it will be passed to uniqueScope and from uniqueScope it will be passed to scopeToGet.

Heavy controller communication in AngularJS

I have implemented a single page application with AngularJS. The page consists of a content area in the middle and sections assembled around the center that show additional info and provide means to manipulate the center.
Each section (called Side Info) and the content area have a separate AngularJS controller assigned to them. Currently, I communicate via $rootScope.$broadcast and $scope.$on(), e.g.
app.controller('PropertiesController', function ($scope, $rootScope) {
$scope.$on('somethingHappened', function(event, data){
// react
});
});
I then call to communicate with other controllers:
$rootScope.$broadcast('somethingHappened', data);
I have quite a lot of communication happening between the Controllers. Especially if something is going on in the content area, several side info elements have to adopt. The other way around is also frequent: a user submits a form (located in a side info) and the content area and other side info elements have to adopt.
My question:
Is there a better way to handle SPA with heavy controller communication?
The code works fine but it is already getting a bit messy (e.g. it is hard to find which events are handled where etc.). Since the application is likely to grow a lot in the next weeks, I'd like to make those changes (if there are any better solutions) asap.
This is really interesting. Pub/Sub should be a right solution here.
You could add extra order to your project by using Angular services as your MVC's model, and update this model for each change. The issue here is that you should implement an observable pattern inside your service and register to them, in order for this to be live synced. So - we're back to Pub/Sub (or other Observable solution that you could think about...).
But, the project will be better organised that way.
For example - SideInfo1Service will be a service/model. Each property change will trigger an observable change which will change all listeners:
myApp.factory('SideInfo1Service', function($scope){
var _prop1;
return {
setProp1: function(value){
$scope.$broadcast('prop1Changed', value);
_prop1 = value;
},
getProp1: function(){
return _prop1;
}
}
});
You could find those really interesting blog posts about using Angular Services as your MVC's model:
http://toddmotto.com/rethinking-angular-js-controllers/
http://jonathancreamer.com/the-state-of-angularjs-controllers/
And, this post is about observable pattern in Angularjs:
https://stackoverflow.com/a/25613550/916450
Hope this could be helpful (:
You have multiple options in order to avoid broadcasts calls:
Share data between controllers using services like it was mentioned in the comments. You can see how to this at: https://thinkster.io/egghead/sharing-data-between-controllers
Create a main controller for the whole page and child controllers for each section (Content Area and Side Info). Use scope prototype inheritance. For example:
if in main controller you have:
$scope.myObject = someValue;
in child Controllers you can set:
$scope.myObject.myProperty = someOtherValue;
you can access myObject.myProperty from your Main Controller
You can use
$rootScope.$emit('some:event') ;
because it goes upwards and rootscope ist the top level
use
var myListener = $rootScope.$on('some:event', function (event, data) { });
$scope.$on('$destroy', myListener);
to catch the event
Then you have a communication on the same level the rootscope without bubbling
Here is my implemented eventbus service
http://jsfiddle.net/navqtaoj/2/
Edit: you can use a namespace like some:event to group and organize your event names better and add log outputs when the event is fired and when the event is catch so that you easy can figure out if fireing or catching the wrong eventname.
Very important question and very good answers.
I got inspired and created three plunks showing each technique:
Broadcasting: http://embed.plnkr.co/lwSNDCsw4gjLHXDhUs2R/preview
Sharing Service: http://embed.plnkr.co/GptJf2cchAYmoOb2wjRx/preview
Nested Scopes: http://embed.plnkr.co/Bct0Qwz9EziQkHemYACk/preview
Check out the plunks, hope this helps.

Use of Meteor-ui-progress-circle (accessing to Template variables created in the HTML)

It may be a very dumb question... I am using Meteor-ui-progress-circle and I want redrawing the template when the percentage (wich is store in a reactive collection Progress) is changed (currently, when I click on a "play" button).
I think I have to use Blaze.render but I don't really understand how it work.
Here a part of my main template (in Jade) :
div.panel-body
div.col-md-9.col-sm-8
p Lorem ipsum...
div.col-md-3.col-sm-4#progress-circle
+progressCircle progress="0" radius="100" class="green"
And my JavaScript :
Template.controlBar.events(
{
"click .play-button": function ()
{
var tmp = Progress.findOne({});
if (!tmp)
{
Meteor.call('createProgress');
tmp = Progress.findOne({});
}
var val = tmp.progressValue;
val += 10;
if (val > 100)
return;
Meteor.call('updateProgess', tmp._id, val);
Template.progressCircle.progress = tmp.progressValue;
Blaze.render(Template.progressCircle, $("#progress-circle")[0]);
},
Doing this... I have several template that are displaying each time I click on the play button. I don't understand how to specify that I don't want a new template but just re-render the one I already have.
Not sure I quite understand your question, but I'll try to help by giving my best understanding of templating and how I have come to use them. If someone sees any incorrect information here, please speak up so I can get a better understanding myself and correct this answer.
First, the Template.XXX.events handlers. In your event handler, you are using a function with no arguments. You can actually accept 2 arguments for these event handler functions: the event and the template. So, you can do something like thus:
Template.controlBar.events({
'click .play_button': function(event, tmpl) {
tmpl.$('div#progress-circle').doSomething();
}
});
Notice the tmpl.$() call? That says to use jQuery to find the specified selector, but ONLY in the current template. This is a wonderful way to use classes to generalize your components, but then be able to filter the selection to only those within the same template...
...Which brings me to my next bit of advice: Use child templates excessively. Any component that I can identify as an "autonomous component" on my page I will consider as a separate template. For instance, I was recently working on a custom reporting page that had a table and some D3 graphs representing some real-time data. In this report page, I had one main template defined for the "page", then each of the D3 graphs where defined as a separate template, and the table was another separate template. This allows several advantages:
Compartmentalization of the "components" of the page, allowing code reuse (I can now put the same graph on ANY page, since it's now an autonomous "component"
The advantage of using the Template.XXX.events trick above to "narrow" the scope of my element searches to elements within that template
Prevents total page refreshes as Meteor is smart enough to only refresh templates that need to be refreshed, which also speeds the responsiveness of the page itself
As a result, I try to apply my Templates liberally. In your case, it would sound to me that if I were to have multiply progress bars on the page that I might turn those into separate templates. I might even do it if I had a single progress bar if it made sense to separate it out for ease of data handling.
Finally, inter-communications between Templates. This can be tricky at times, but the best, most efficient way to do this I have found is through Session variables. The pattern I typically use is to have my data for my template be returned by a Template .helper, which does something like this:
Template.controlBar.helpers({
progressData: function() {
if (!Session.equals('playId', null)) {
return Progress.findOne({_play_id: Session.get('playId')});
}
}
});
Because Helpers are reactive, and Sessions is reactive, the template is re-rendered anytime the 'playId' is altered in the Session. The corresponding Session variable can be set from anywhere in the client code. Again, this tends to work best when you narrow the scope of your templates to the individual components. It is important to note here that the Session object in Meteor is NOT the same as "sessions" in other languages like Java and such, which typically use cookies and a session token/id. Meteor sessions work considerably different, and do not survive page reloads or closing of browsers.

How to have an angularJS directive on a page called via ajax?

I have the following html (which can be accessed directly or called via ajax):
<section id="content" ng-controller="setTreeDataCtrl" get-subthemes>
<dl ng-repeat="subtheme in allSubthemes">
<dt>{{subtheme.Title}}</dt>
</dl>
Then I'm using the following directive:
myApp.directive('getSubthemes', function() {
return function($scope, element, attrs) {
$scope.allSubthemes = [];
angular.forEach($scope.data.Themes, function(value, key) {
angular.forEach(value.SubThemes, function(value2, key2) {
$scope.allSubthemes.push({
'ThemeTitle': value.Title,
'ThemeUrlSlug': value.UrlSlug,
'Title': value2.Title,
'UrlSlug': value2.UrlSlug
});
});
});
}
});
$scope.allSubthemes seems ok, but the dl's don't get rendered.
I can see for a second everything rendered properly and then it get's back to {{subtheme.Title}}, almost like it's being "unrendered"... any ideas of what I'm doing wrong?
Demo jsFiddle: http://jsfiddle.net/HMp3a/
rGil fixed the jsFiddle. It was missing a ng-app="pddc" declaration on an element so Angular did not know where to begin its magic.
I'd like to mention another way to render to the data in question. I suggest using an ng-repeat within an ng-repeat. See my forked & updated fiddle here. You can actually refer to the parent theme within the ng-repeat of the subtheme, so you don't have to copy values from the parent theme into each subtheme (which effectively eliminates the need for the directive in this example).
Another reason to use a nested ng-repeat is because of async issues that could come up when pulling data from a web service asynchronously. What could happen is when the directive executes, it may not have any data to loop through and populate because the data hasn't arrived yet.
If you use two ng-repeats, Angular will watch the $scope.data and re-run the ng-repeats when the data arrives. I've added a 500 ms delay to setting the data in my example to simulate web service latency and you'll see that even with the "latency", the data eventually renders.
There are two other ways around the async issue:
Use scope.$watch() in your directive, to watch for the data manually, or
Use the "resolve" functionality from Angular's routing feature to make sure the data is retrieved prior to controller execution.
While these alternative methods work, I think both are more complicated then just using two ng-repeats.

Stop AngularJs from creating new controller/$scope cache and use cached one

Using $routeProvider every time user clicks on a link, a new $scope is being generated. That means all the data is lost. How can i make Angular use the same controller/$scope?
Explanation:
http://jsfiddle.net/mpKBh/1/
(click on links)
<a href='#'>First controller</a>
<a href='#/view'>Second controller</a>
$routeProvider.
when('/', { template:"{{$id}}",controller: ContentListCtrl}).
when('/view', {template:"{{$id}}",controller: ContentDetailCtrl}).
P.s. is it possible to know which controller is currently active?
In AngularJS, $scope is not meant to hold data that persists across your application. For that, you want to use a service that is injected into both controllers. If you provide more detail on what data you're missing across routes, I would be happy to revise this answer to include something a little more actionable.
In re your PS: You can inject the $route service to get information about the current route; the $route.current.controller property will give you the constructor function of the current route.
For those researching how to "unbind" in AngularJS, he is a bit of info (related to the OP's last comment above)
When a view is destroyed, it's basically marked for garbage collection - but it's still there. That is why you are getting multiple requests when a scroll happens - because it's still listening for events.
So the easiest way to deal with this (that I have found, though I'd like to learn of other ways) is to listen for the $destroy event and react on it.
You can "unbind/unlisten" for an event by keeping a reference to what is returned by the $on method. Here is an example taken from a controller:
$scope.systemListener = $rootScope.$on("someEventYouListenTo", function (event, data) {
console.log('Event received by ' + $scope.name);
});
$scope.$on('$destroy', function () {
// Remove the listener
$scope.systemListener();
});
Now those old scopes/views won't react to events anymore.
Hope that helps someone!

Categories

Resources