Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I replace my angular location without a controller refresh?

Suppose I have an Angular app for editing eCards. Creating a new eCard uses a path like #/ecard/create and editing an existing eCard uses a path like #/ecard/:id. A tabbing system lets us have multiple eCards open for editing at a time.

We'd like an autosave feature like what users would expect from e.g. modern webmail or wiki software (or StackOverflow itself). We don't want to save an eCard draft the moment the user opens the Create form, which would give us a lot of drafts of blank eCards, so we start autosaving once the user starts typing.

I'd like to write code like this in our controller (this is simplified to not include e.g. error handling or stopping the autosave when the tab is closed, etc):

$scope.autosave = function () {
    ECardService.autosave($scope.eCard).then(function (response) {
        $location.path('/ecard/' + response.id).replace();
        $timeout($scope.autosave, AUTOSAVE_INTERVAL);
    });
};
$timeout($scope.autosave, AUTOSAVE_INTERVAL);

The above code works great, except for one thing: when the location changes, our controller reloads and the view re-renders. So if the user is in the middle of typing when the autosave completes, there's a brief flicker, and they lose their place.

I've considered several approaches to mitigate this problem:

1) Change the path to use the search path and set reloadOnSearch to false in the ngRoute configuration. So the path would change from #/ecard?id=create to e.g. #/ecard/id=123 and thus not force a reload. The problem is that I might have multiple eCards open and I do want changing from e.g. #/ecard/id=123 to #/ecard/id=321 to trigger a route change and reload the controller. So this isn't really feasible.

2) Don't bother editing the URL and deal with the back button giving a weird behavior in this case. This is tempting, but if a user opens their list of existing eCards and tries to open the specific eCard that has been saved, we want the tabbing system to recognize that it should just display the currently existing tab rather than open a new tab.
We could theoretically address this by updating our tabbing system to be smarter; instead of just checking the path, it could check both the path and the persistent id, which we could store somewhere. This would make the tabbing system significantly more complex, and that seems like overkill for this feature.

3) Only change the URL when the user is not actively editing, e.g. write a $scope.userIsIdle() function which returns true if it's been at least 10 seconds since the user made any edits, then update the path based on that. A simplified version of this would look something like:

$scope.updatePathWhenSafe = function (path) {
    if ($scope.userIsIdle()) {
        $location.path(path).replace();
    } else {
        $timeout(function () {
            $scope.updatePathWhenSafe(path);
        }, 1000);
    }
};

I ended up going with option #3; it was significantly simpler than option #2, but a lot more complicated to implement and test than I'd like, especially once I account for edge cases such as "what if the tab is no longer the active tab when this timeout fires?" I'd love for option #4 to be possible.

4) Go outside Angular to edit the current location and history, assuming this is necessary and possible. This would be my preferred solution, but my research indicates it's not safe/advisable to try to go around the $location service for changing your path or editing history. Is there some safe way to do this? It would make things so much simpler if I could just say, "Change the current path but don't reload the controller."

Is option #4 possible/feasible? If not, then is there a better way? Maybe some magical "Do it the angular way but somehow don't refresh the controller"?

like image 615
Eli Courtwright Avatar asked Mar 04 '14 16:03

Eli Courtwright


People also ask

How to update the URL in AngularJS?

If the user is maximizing it the first time, share the url to this maximized view with the maximizedWidgetId on the UI. As long as you use $location(which is just a wrapper over native location object) to update the path it will refresh the view. Not only $location will refresh the view.

What is HTML5 mode?

In HTML5 mode, the $location service getters and setters interact with the browser URL address through the HTML5 history API. This allows for use of regular URL path and search segments, instead of their hashbang equivalents.

How to reload a page in AngularJS?

Reload Page Using location.reload() in AngularJS reload() method is when a user clicks the refresh button or presses F5 on their keyboard. It reloads the current page and clears any cookies set in the previous request to this server. It also causes all images, stylesheets, scripts, and other files to be reloaded.


3 Answers

This is not angular way, but it can be useful. After receiving data you can check whether there is an focused element (user is typing). If so, then you need to define a function that is performed once when element lose focus. If no focused element, the change url immediately.

Like this:

ECardService.autosave($scope.eCard).then(function (response) {
    if($(':focus').length){ //if there is focused element
        $(':focus').one('blur', function(){ //
            $location.path('/ecard/' + response.id).replace(); //perform once
        });
    }
    else{
        $location.path('/ecard/' + response.id).replace();
    }
});

Of course this is not the most elegant solution, but it seems to solve your problem.

like image 72
Goodnickoff Avatar answered Oct 18 '22 19:10

Goodnickoff


If you have code that needs to run across multiple view controllers AngularJS provides a root scope for such instances. You can find the documentation here.

However I would recommend against having a tabbing system that is actually multiple views. Having multiple items open means to have them all in your work space.

You might want to consider a single view with Angular directives for your e-cards. That way they could each have their own scope and would be available at an instance without re-rendering the page.

They would also be able to share the functions defined in the controller's $scope, without the need for an app wide root scope. Note that scope has to be enabled on directives. scope: true

Check out the AngularJS site for tutorial and documentation on this.

like image 38
Marc M. Avatar answered Oct 18 '22 21:10

Marc M.


It seems that the best solution for the problem you're describing would be to use a state machine like ui-router.

With a library like that one, you can have a single page app that has multiple states (that you can also make part of the url), so whenever the state changes, you can save your e-card and you'll never have any visible reloads because you're working on a single page application.

like image 1
ruedamanuel Avatar answered Oct 18 '22 20:10

ruedamanuel