Peter Wone
Peter Wone

Reputation: 18775

App-wide state with RequireJS

Durandal views are AMD modules. The shell is a special case view, and is also a module.

It was my understanding that if I wrote a module to return an object instance then I would get a reference to the same instance in any module that requires it; whereas for modules returning a constructor function, the constructor would be invoked to yield a private instance.

I have a login view which (surprise, surprise) authenticates the user and surfaces a property IsAuthenticated. This is a knockout observable, and various pieces of UI bind to it so that when login.IsAuthenticated() returns false, you get the login UI and when it returns true you get whatever it was for which authentication was a precondition.

All this works, but only once. When I added a logout capability that expires the session token at the server and sets login.IsAuthenticated(false) at the client, all the UI that successfully responded to logging in totally fails to respond to logging out.

The logout capability is implemented in the shell, because it's app wide. The start of shell.js looks like this:

define(['plugins/router', 'knockout', 'config', 'viewModels/login'],
  function (router, ko, config, login) {
  var shell = {
    login: login,
    check: function () {
      alert(login.IsAuthenticated());
    },

The check method is there because in the process of figuring this out I put a button in the shell that calls check to tell me what login.IsAuthenticated() returns. Experimental results suggest that each module that references login seems to get a copy with values as at the time of importation.

I think the problem here is misapprehension of the behaviour of RequireJS.

What is the correct way to go about implementing this kind of app-wide state?

Upvotes: 0

Views: 278

Answers (1)

Peter Wone
Peter Wone

Reputation: 18775

The answer is to put the app state in the app object.

In accord with Louis' observation that something is wrong if singletons don't work, there is a problem with declaring a dependency on a viewmodel in the shell viewmodel.

If you do then then you end up with two copies. I don't know why, but it happens, and it's this that underpins all the weirdness I've been seeing.

So if you can't do this then what can you do?

There's actually no need for the shell to reference any view model.

Certainly there are occasions on which a view and view-model exist to allow the user to manipulate application state such as settings or authentication.

In such cases, the information actually belongs to the application as a whole: the app object is the model. This is not only logically correct, it makes the problem go away altogether. For example, my login view model is now backed by the observable app.IsAuthenticated() which is already in scope in the shell.

Now, the shell view does require this observable to exist before the binding phase. It also requires a name for the app object that's in-scope for the binder, so in the shell implementation I set these up.

define(['durandal/app', 'knockout', ...], (function (app, ko, ...) {

  app.IsAuthenticated = ko.observable();

  var shell = {
    app: app,
    ...
  };

  return shell;
}

and Bob's your uncle! With this infrastructure in place you can protect content with a wrapper as thin as this.

View model

define(['durandal/app'], function (app) {

  return {
    activeView: ko.computed(function () {
      return app.IsAuthenticated() ? 
        "viewmodels/upload-queue" : 
        "viewmodels/login";
    }, null)
  };

});

View

<div data-bind="compose: activeView"></div>

Notice the incredibly simple placeholder mark-up. Back to the shell, we're now well placed to use this information in our mark-up:

<div class="btn btn-default pull-right" 
     data-bind="click: app.logout, visible: app.IsAuthenticated">
  <i class="icon-signout" title="Log out"></i>
</div>

Astute readers will be wondering how app acquired a logout method. Actually this is a method of the login object, and it's assigned to app in the login view-model module like this:

app.logout = login.logout;

Upvotes: 1

Related Questions