2

Abstract

Hi, I'm using angular + ui-router in my project, I have huge amount of nested states and different views that in turn contain huge amount of different inputs, a user fills these inputs incrementally step by step.

The problem

Sometimes users require additional info that is located on the previous step, and browsers "back" button helps the user to sneak peek into that data, but as soon as the user presses it, the info he already entered is lost due to state transition, which is obviously a bad thing.

Strategy

In order to overcome described problem I have the following plan:

  1. Associate each user's "navigation" (guess this is a proper term) with a random id
  2. To prevent scope-inheritance-and-serialization issues, instead of putting viewmodel into $scope use ordinary javascript object that will be storing immediate values that are bound to UI.
  3. Add watcher to look for changes on that "storage object"
  4. As soon as the change spotted, serialize the object and persist it

Explanations

Why do we need a random parameter in URL?

We don't want to store all data in URL, since there might be quite some amount of data that wont fit into URL. So in order to provide the guarantees the URL won't break, we put only small random GUID/UUID into it that later allows obtaining the data associated with current "navigation" by this random GUID/UUID.

The storage

There are multitude of storage scenarios available out there: LocalStorage, IndexedDB, WebSQL, Session Storage, you name it, but due to their cross-tab, cross-browser, browser-specific nature it would be hard to manipulate and manage all of the data that gets into the storage. The implementation will be buggy / might require server-side support.

So the most elegant storage strategy for this scenario would be storing data in special window.name variable which is capable of storing data in-between requests. So the data is safe until you close your tab.

The Question

On behalf of everything written above, I have the root view called "view" that has a state parameter id (this is the random GUID/UUID)

$stateProvider.state('view', {
    url: '/view/{id}',
    controller: 'view',
    templateUrl: 'views/view.html'
});

All of the other views derive from this view, is there way to make ui-sref directive to automatically inject a random GUID/UUID into id state parameter of my root view, instead of writing each time ui-sref's like:

<a ui-sref="view({id:guid()}).someNestedView({someNestedParam: getParam()})"

I would like to have something like:

<a ui-sref="view.someNestedView({someNestedParam: getParam()})"
Community
  • 1
  • 1
Lu4
  • 14,873
  • 15
  • 79
  • 132
  • If all the other states are children of the state 'view' then you could store the object within the scope of the 'view' controller. You could make changes to the object within that scope from the nested scopes and the changes would persist even as you navigated through the children. You could use 'controllerAs' syntax to be clear what data is bound to the 'view' scope and what is bound to the child scope too. Also ui-sref has a sibling view shortcut "^.sibling" :) – horyd Oct 14 '14 at 00:30
  • Can you decorate the state variable to contain the previous state? Or have a state to listen to state change begin and put the current state information on the state object in case the next state that's being transitioned to needs it? Unless I'm missing something you don't need that complicated solution to check the previous state. – dchhetri Oct 14 '14 at 04:11
  • @horyd I'm not sure that I understood the details of your proposition, how do you restore previous state after user clicks browser's back button? – Lu4 Oct 14 '14 at 09:57
  • @user814628 do I understand correctly, by "state variable" you mean state parameter that is stored on URL? I can listen to state-change-begin events, but how can I know whether we are moving forward or backward in the hierarchy, there seems no information on what caused state change event. – Lu4 Oct 14 '14 at 10:10
  • If the back button navigates the user between child states of the state 'view' then the 'view' controller is not reinitiated. Data held within that controller will persist – horyd Oct 15 '14 at 02:32
  • Probably I will need to think harder on scenario you've proposed, it would be great if GUID/UUID parameter could be taken off the URL. Though at this point I don't see the whole picture on how to achieve that. The main problem I see right now is: 1) identifying whether we are moving forward or backward 2) intercept `move forward` / `move backward` events Ok, suppose we could use the decorator pattern and intercept every `$state.go(...)` call, it would be a sign of the fact that we are moving forward, otherwise backward – Lu4 Oct 15 '14 at 09:14
  • I will need to play a little bit before knowing for sure – Lu4 Oct 15 '14 at 09:21

1 Answers1

2

The AOP and Decorator pattern are the answer. The comprehensive description could be found here:

Experiment: Decorating Directives by Jesus Rodriguez

Similar solution as described below, could be observed:

Changing the default behavior of $state.go() in ui.router to reload by default

How that would work? There is a link to working example

In this case, we do not solve from which source the random GUID comes from. Let's just have it in runtime:

var guidFromSomeSource = '70F81249-2487-47B8-9ADF-603F796FF999';

Now, we can inject an Decorator like this:

angular
.module('MyApp')
.config(function ($provide) {
    $provide.decorator('$state', function ($delegate) {

        // let's locally use 'state' name
        var state = $delegate;

        // let's extend this object with new function 
        // 'baseGo', which in fact, will keep the reference
        // to the original 'go' function
        state.baseGo = state.go;

        // here comes our new 'go' decoration
        var go = function (to, params, options) {
            params = params || {};

            // only in case of missing 'id'
            // append our random/constant 'GUID'
            if (angular.isUndefined(params.id)) {

                params.id  = guidFromSomeSource;
            }

            // return processing to the 'baseGo' - original
            this.baseGo(to, params, options);
        };

        // assign new 'go', right now decorating the old 'go'
        state.go = go;

        return $delegate;
    });        
})

Code should be self explanatory, check it in action here

Community
  • 1
  • 1
Radim Köhler
  • 122,561
  • 47
  • 239
  • 335