is angulars reloadOnSearch evil? - angularjs

I've been programming with angularjs for some time now, however, i started using reloadOnSearch for my application and at first i thought this was going to save me for uneccesary reloads of controllers and ajax calls against my API.
For example say i have a list of things, and then present them in a view. When you first access this view it fetches the list of items from the API and presents them, and when you click on an item it adds the ?id=xxx&view=show query parameter to the url without reloading the controller.
When we access a single item from the already loaded list, it just fetches the list item from that we already have and presents it. However, if we forcibly reload the page, the controller now realizes that it has to fetch it from the API instead as it doesn't exist within the list collection.
At first this seemed like a great thing. However as i think about it, i now have to manage the state of the entire controller, whereas before i could have state enclosed within single functions in the controller.
I seem to be having trouble deciding if reloadOnSearch is evil and should be avoided, or if it is worth keeping around. What is your opinion?
Also, would it be better to use something like ui-router instead? I just saw a introduction video which implies that one can have better control of state.

I've converted my app to use ui-router instead. which basically nests application logic in "sub scopes" making it possible to share data between states/pages. I realize now that this is possible without ui-router aswell, however one would have to create pages that has a hierarchy of <div ng-controller>
So in conclusion, the way i was using reloadOnSearch was indeed evil. However, there were better ways of using it aswell.

Related

Delay generating directive until after page is ready and responsive

I'm working on a single-page app where some parts are really slow. They're slow because I'm displaying 400 complex things in a repeater for the user to scroll through. Each thing is generated by a fairly complex directive that does a ton of data binding and expression evaluation, adds one or two click handlers, and displays a couple of images. In some cases, I also need a grayscale CSS filter on those images, but that really seems way too slow.
I have of course already turned most of my data binding into one-time data binding, but simply generating the 400 things for the first time is still slow. It's initially hidden through ng-if, which speeds it up when I'm not showing it, but once I do need to show it, everything waits 10 seconds for that to happen. I would like to load it in advance, but using ng-show instead of ng-if means the loading of the entire app has to wait for this.
What I would like, is to load the rest of the app, and then, while we wait for user input, start creating these 400 things so they're ready once I need to show them. I don't want the user to notice how slow this is.
Problem is, I have no idea how to do this. Any ideas?
Edit: Having thought about this (and discussed this with my wife), I'm seeing two options (that I conceptually understand, at least):
The easy, quick, boring and cowardly solution is to simply not show the 400 things at the same time, but cut them in pieces and show 40 at a time. That should make it a lot quicker, but also less nice, as the user needs to click around to access all the data.
The interesting solution is to write a new ng-repeat that generates the 400 transcluded copies of the template each in their own asynchronous event, so they don't block user interaction. I really like this idea, but there's one big downside: it's an ambitious idea with deep Angular magic, and I don't have much time available.
OK, not seeing your code structure, through Q&A, I'm trying to get clarification. If I understand you correctly, I believe the solution is to process your images asynchronously and remove reliance of creating/processing them on the fly when the view is visible (i.e. via clicking on a button/tab to 'show' the array 'view' and thus triggering the ng-repeat). BTW, this solution assumes the delays are because the images are being processed rather than because they are being shown.
METHOD 1 (less preferred)
To do this, it's best to create an 'ImageDataService' service, where it get's kicked off at application start, and proceeds with building this array and taking whatever time it needs asynchronously without caring what view is showing or not. This service will be injected into the proper view or directive controller--perhaps where the current ng-repeat scope is.
Meanwhile, you also need to change the directives inside your ng-repeat to get the pre-built data from the array provided by ImageDataService rather than actually doing the calculation at view time. If the data for that directive is not ready, the directive will show something like 'loading...' otherwise, the directive will simply show the prebuilt images. This way, it doesn't matter when ng-repeat is triggered (i.e. its view is showing), because it will just show what are processed and ready at that point and the display the rest as 'loading...'.
METHOD 2 (I prefer this)
Alternatively, and maybe better, you can forego creating a pre-processed array in a service as in METHOD 1 and instead modify your directives to process their data asynchronously by invoking a service method that returns an asynchronous promise like this:
(code inside a controller)
var promise = ImageDataService.processImage(directiveData);
promise.then(function() {...set the directive image attributes..})
Needless to say, the ImageDataService.processImage() method needs to return a promise when it is done processing the data. The directive, as usual, will show 'loading...' until then. Also, although ImageDataService no longer needs to pre-populate its array mentioned in METHOD 1, it might be a good idea to save the processed images to a similar array anyway to serve as cache and not reprocess them needlessly. NOTE, in this case you don't need to have processImage() inside a service--but it is really good 'separation of concerns' practice to reserve the work of asynchronous data processing (ajax, etc) within a service which can be injected app-wide rather than within a controller.
Either of these 2 general tacks should work.

Splitting functionality across controllers that must interact

We are using Angularjs and ui-router. We generally have a layout of each page that utilizes views. We have a filter view, sort view, and pagination view; as well as display views that can be swapped in and out.
Logically when changes are made we need to any of theses controllers we need to update the displayData as appropriate. Changes to filter should run the filterMethod, but also need to run sort and then pagination logic afterwards, while changes to sort should run just pagination after, making a clear order of operations for when each controller needs to it's update.
My problem comes when I consider that in some cases we may not want to utilize all 3 controllers. We may want filtering, but not pagination for example.
We are having trouble finding a clean way to make these controllers 'just work', so that we can plug in whichever control we want in uirouter and have them function. The problem is mostly one of scoping. If I do the obvious thing, and have each controller define their own updateData method when changes are made to it, I run into scoping problems if I want them call the next controller's update afterwords. The filter controller can't call sort because the two controllers don't share a scope. I can use broadcasts, but what if I want a filter and a pagination controller, but not a sort? How do I ensure that sort runs before pagination if both are present, but if sort controller doesn't exist pagination knows to run after filter?
I could instead move everything up to my top level controller, and then things just work. However then I end up with a controller that feels like it's doing way to much, It's cleaner to have one controller for each type of control if possible.
We have other approaches we could use, but they feel like their making pretty strong presumptions about our controller scheme. If I later added some fourth controller I would have to modify everything because each controller is really hard coded very explicitly with presumptions about how the other's run.
This seems like a common issue. Is there a a best practice or convenient technology for handling splitting of functionality across controllers?
Hoist the data, not the display logic. There are only two ways to share data between controllers cleanly: a service, and a parent controller.
If what you were sharing was data (eg: displayData), I might suggest a service object, but sounds more like application state (eg: orderBy), so I think these "settings" should live on a parent controller.

Angularjs: Find all instances of a directive

I'm trying to create a directive to allow the user to navigate the page with arrow keys by section. But I also want to be able to have those sections be scattered around the dom, and to have this not break when stuff gets added and removed. I can think of several ways to do this, but none of them are satisfactory:
Create a directive with a controller that lets other directives register themselves (and unregister on $destroy). But this will be out of order if I add something in the middle later. Also, I've tried writing it this way, and it seems like way more code than necessary.
Whenever the user hits an arrow key, make an empty array, and $broadcast an event, with a callback for directives to register themselves on that list. Then, once that list is full, advance or go backwards on it. They (should?) come back in the order they're in on the DOM, but I'm not sure since this way seems crazy and hackish.
Mark things that are 'tabbable' with css, and write this the simple way in jquery, something like this: On a new click event, var all = $('.tabbable'), and then do the obvious with that. But I really don't want to do it that way, because it's not 'the angular' way. Not out of some sense of purity, but because I'm building this as part of a larger library of widgets, and I want this functionality to be accessibly to them.
So, is there any way for me to get the scopes of all directives of a certain type, without resorting to weird hacks, or spreading the logic out all over the place?
This is a good question. +1
First, finding all directives or nodes by type goes against the Angular way. The View is the official record in AngularJS, so directives should say what they do and do what they say. Coding some process somewhere to scan for DOM nodes and act accordingly is problematic for several reasons, not the least of which are separation of concerns and testability.
I'm glad to see you're looking at other options, but I agree that the other options you provided are sub-optimal for the very reasons you mentioned. But I have one more. This is one that I've used for a different application, but that required knowledge of scattered DOM nodes.
First, we create a service to manage the state of this component. It's simple. Let's call it SectionsService. Next, we create a directive to register sections. Let's call that section for simplicity. The section directive registers the DOM node's ID (maybe created programmatically to ensure uniqueness) with the SectionsService during its linking phase. Since the DOM is processed (mostly) in order, the nodes added to the SectionsService will also be in order. So the DOM looks something like this (irrelevant stuff omitted):
<div section>...</div>
<!-- other stuff -->
<div section>...</div>
<!-- other stuff -->
<!-- etc. -->
(Though out of scope here, it would not be very difficult to program it in such a way that the order wouldn't matter, but it'd be based on specifics of your app that I don't know.)
Next, you create your triggers, like an arrow key handler. On these events, you simply tell the SectionService to go to the previous/next node in the list. AngularJS comes with a service called $anchorScroll that can be used to emulate the browser's hash-based positioning we're familiar with. You could obviously also use a jQuery plugin to animate the scrolling if you wanted to.
And that's it! A very simply directive, a fairly simple service, and whatever trigger(s) you need. All told, I'd guess less than 100 lines of code including tests. All components are decoupled and easily testable, but still really quite simple. The view remains The Truth. The Angular Way is preserved.
And there was much rejoicing.
I hope this sets you on the right direction, but of course feel free to ask a follow-up question. We can also talk code specifics too if you'd like; as I said, they wouldn't be very complicated.
AngularJS services are singletons and can be required via dependency injection. You could have your directives require a state manager service and call incrementers/decrementers.
Alternatively, a little easier but more brittle, you could keep an array in $rootScope. It's more idiomatic "angular" (but not by much) than a jquery selector global, but probably not the best route if you're building a widget library.

How do I create a new, uh, thing in Angularjs?

I think the vagueness of the question is part of the problem, so my real first question is, what do you call, in Angular, the thing.
The thing I'm trying to name is the view plus the controller, over the model of a single object. I don't even know what to call it. For things I know ahead of time I'm going to need, I've been creating directives, but what do you call one instance of the thing that a directive creates?
I have several situations where all of a sudden (in response to some external event), I have a new object in the model and I want to show it on the screen. Angular seems to want me to list all the possible views ab initio in their parent view, but that isn't really reasonable in my case. How, for example, would I list all the pop-ups and tool-tips and other stuff.
I'm down in some little edge case, deep in the controller code, and it needs to add something to the current view. What's the accepted practice.
Incidentally, the $route/ng-view is one case of exactly this. The view containing the ng-view, and the ng-view DIV itself, have no idea what the $route module is going to put in the ng-view. I need the more general case of this strategy.
EDIT
People keep asking for an example. How about this: I'm making an equipment-requisition app. When a user asks that one of the 1000 different type of equipment be sent to him, I need to display a pop-up that gathers addition information specific to that type. If he asks for a screwdriver, the pop-up will ask about blade size, neck length, and handle composition; if he asks for an airplane, it will be a wizard ask him about engine size, fuel tanks, seating arrangement. All the app knows on start-up is the list of all equipment types, and the name of the UI element that gathers all subsequent information about each particular type.
I'm down in some little edge case, deep in the controller code, and it needs to add something to the current view. What's the accepted practice.
Somewhere, you need to define all of the views you'll need -- e.g., all of the equipment popups. You could put each view into a separate file and use ng-include to dynamically pull in the one you currently need to display. Define a property on your $scope (e.g., $scope.equipmentTypeViewUrl), then
<div ng-include src="equipmentTypeViewUrl"></div>
Since ng-view can only appear once per page, ng-include is probably not what you need to use if you need multiple levels of routing.
See also
create a single html view for multiple partial views in angularjs
https://groups.google.com/forum/#!topic/angular/xIIyGpW8KUk/discussion
App design using Angular js
Client Side Template with view per role
I think the problem is that you think that you need to create the "thing" in controller, but actually you don't. The way two-way data binding works is that you change some attribute value, and the view changes based on that. I've never seen a use case where that's not enough, pop-ups and tooltips notwithstanding.
However, if you really must have the controller show something, you could utilize angular's events to do that. You would need two parts: a directive responsible for showing stuff (modifying DOM), and the controller. The controller would $broadcast an event with some parameters, and the directive would listen to those events using $on and react accordingly.
I'd just make sure I had some useful code coming in as the model...
<div class="row" ng-repeat="attribute in attributes">
<div class="widget" ng-repeat="input in attribute.inputs">
<input type="{{input.type}}" ng-model="input.value" />
</div>
</div>
I'm extremely limited in my knowledge, but all I know is if you have a definite structure to your model you can build a view that reacts to it dynamically.
If all of those things are related to your original object (properties or in some way other) you could loop through the data, display the properties and if need use the keys and filters for a label. Imho it's not really an angular question, more one if your data structure. If you have a good data structure you could use a service for creating a related data-object.
For a related popup, you can use a directive and even process the model data there (only recommended if it has a consistent structure).
If you dislike this approach, you can process the data directly in the template.
But without more specific details, there will be no definite answer.

Preferred way of creating links with backbone.js

I'm trying to wrap my head around backbone.js but I'm finding it hard due to the lack of (IMO) good examples.
First of all, what is the best way of getting a link to an object.
If I want to get the edit url of an Album model I could do album.url() + '/edit', is this really the best way?
Also, I'm trying to make my application work 100% without javascript so I don't want my URLs/links to say /albums/#1/edit, I want it to be /albums/1/edit and override this in JS.
I'm thinking I create normal URLs and use jQuery.live to call router.navigate in backbone.js
I never got this to work however, when I call router.navigate('/albums/2', true) the URL changes but my show action is never called. If I refresh it's called so the route is matched.
What am I missing?
The basic answer, which is kind of frustrating, is "there is no preferred way!". Backbone.js doesn't tell you how to set up links, you can do it any way you like. I found this flexibility just as annoying as you do, at least at first.
So here's the way I'm approaching this on my current project, with the (big) caveat that this is just one of many ways to do things in Backbone:
For the most part, I don't use actual links. There's no explicit reason not to, but it means you have to keep track of a bunch of URL strings that have to be consistent. I would rather stick all the URL formatting in my routers and not deal with it elsewhere.
To open a new "top-level" view, like an editing screen, I set something that fires an event. In the application I'm currently working on, I have a global State model, and to open a new view I call state.set({ topview: MyTopView }). This causes the state object to trigger change:topview.
Any piece of the UI that needs to change when the top-level view changes has an update method bound to change:topview. When the event fires, they look at state.get('topview') and update as necessary.
I treat my routers as only marginally specialized parts of the UI - they're essentially views that render in the browser address bar, rather than the window. Like other views, they update the state object on UI events (i.e. a new URL), and like other views, they listen to the state object for changes that cause them to update. The logic that the editing screen has the URL albums/<albumid>/edit is fully encapsulated in the router, and I don't refer to it anywhere else.
This works well for me, but it adds an entirely new pattern, the global State object, to the Backbone structure, so I can hardly call this the "preferred" approach.
Update: Also note that .url(), in the Backbone idiom, refers to the model's URL in the back-end API, not the front-end URL (it's not like Django's get_absolute_url). There is no method in the default Backbone setup that gives you a user-facing URL for your model - you'd have to write this yourself.
Also, I'm trying to make my application work 100% without javascript; so I don't want my URLs/links to say /albums/#1/edit, I want it to be /albums/1/edit and override this in JS.
you can do exactly this w/ pushState. just enable it in your Backbone.history.start call:
Backbone.history.start({pushState: true})
this tells Backbone to use the HTML5 History API (a.k.a. "PushState"), which uses full URLs exactly like you're wanting.
read up on the history api here: http://diveintohtml5.ep.io/history.html
and I wrote up a 2 part series on using pushstate w/ the second part focusing on progressive enhancement in backbone, to do what you're needing:
http://lostechies.com/derickbailey/2011/09/26/seo-and-accessibility-with-html5-pushstate-part-1-introducing-pushstate/
and
http://lostechies.com/derickbailey/2011/09/26/seo-and-accessibility-with-html5-pushstate-part-2-progressive-enhancement-with-backbone-js/
hope that helps :)

Resources