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"?
tabbing system
works it difficult to look into this option further. You say the tabbing system can have multiple eCards open for editing, how do you distinguish between the different eCards if you say you use the path/ecard/:id
for editing? – Sechrist$location.path
inside thethen
function ofautosave
? As I understand it, the location shouldn't change unless the tab changes. I would assume the autosave function is responding with a response from the current tab, so theid
ofresponse
should be the same as the currentid
. – Unpriced/ecard/new
to/ecard/123
. – Balladeer/ecard/123
or/ecard/42
- I was using:id
to indicate that we actually put the id in the path, which we then get with the$routeParams
service. – Balladeer/ecard/123
to/ecard/42
. The tabs themselves are basically just links with an href to#/ecard/:id
. – Balladeer