1

I would love to a #signin route that would open a dialog on top of whatever page there was before.

Let's consider this example app this the following routes:

router.map([
    {route: '', moduleId: 'vm/home', title: "Home"},
    {route: 'about', moduleId: 'vm/about', title: "About"},
    {route: 'signin', moduleId: 'vm/signin', title: 'Sign In'}
]);

Here are example use cases:

  1. User is on # and navigates to #signin: we should see a Sign In dialog on top of Home page

  2. User is on #about and navigates to #signin: we should see a Sign In dialog on top of About page

  3. User navigates to http://localhost:9000/#signin: we should see a Sign In dialog on top of Home page

  4. User is on #signin and closes dialog: we should see a page that was behind the dialog (there's always a page behind).

Dziamid
  • 11,225
  • 12
  • 69
  • 104
  • possible duplicate of [How to create "full page dialog" with DurandalJS](http://stackoverflow.com/questions/18537647/how-to-create-full-page-dialog-with-durandaljs) – Gildas.Tambo Oct 03 '14 at 09:51
  • @Tambo, it is not a duplicate, op is looking for a dialog without a route, where as I am looking for a solution where dialog is something like a `page` that has it's own route. – Dziamid Oct 03 '14 at 13:18

3 Answers3

2

The dialog and router are both plugins and have no interactions between eachother.

Also having the router display dialog would ignore how the router works - it has a div which it dumps content into. Dialogs exist outside of all of this.

However if you wanted to (I may do this aswell), you could try this.

Add dialog: true to the route map.

Override router.loadUrl method. Check if the route is a dialog route as we marked before, and activate the dialog instead.

I would make the dialog a child route, so then you can know which view to display beneath the dialog. Otherwise you could just have to show the dialog over anything and ignore routing entirely.

Edit: I don't think this would entirely work actually. loadUrl returns a boolean. You could open the dialog and return false to cancel navigation.

Edit2:

My Attempt

The loadUrl method loops through all routes, and each has a callback, so ideally we need to insert our logic into this array.

for (var i = 0; i < handlers.length; i++) {
    var current = handlers[i];
    if (current.routePattern.test(coreFragment)) {
        current.callback(coreFragment, queryString);
        return true;
    }
}

This array is added to using the routers route method. Durandal calls this method when you map routes, so ideally we could add some extra parameters to the route config and let Durandal handle these. However the configureRoute function is internal to the routing module, so we will need to edit that and make sure we copy changes over when updating Durandal in the future.

I created a new list of dialog routes:

{ route: 'taxcode/add(/:params)', moduleId: 'admin/taxcode/add', title: 'Add Tax Code', hash: '#taxcode/add', nav: false, dialog: true, owner: '#taxcodes' },
{ route: 'taxcode/edit/:id', moduleId: 'admin/taxcode/edit', title: 'Edit Tax Code', hash: '#taxcode/edit', nav: false, dialog: true, owner: '#taxcodes' }

The idea of an owner, is that if there is a case where the initial route is this, we need something behind the dialog.

Now replaced the router.route call in configureRoute with this:

router.route(config.routePattern, function (fragment, queryString) {
    if (config.dialog) {
        if (!router.activeInstruction()) {
            // No current instruction, so load one to sit in the background (and go back to)
            var loadBackDrop = function (hash) {
                var backDropConfig = ko.utils.arrayFirst(router.routes, function (r) {
                    return r.hash == hash;
                });
                if (!backDropConfig) {
                    return false;
                }
                history.navigate(backDropConfig.hash, { trigger: false, replace: true });
                history.navigate(fragment, { trigger: false, replace: false });
                queueInstruction({
                    fragment: backDropConfig.hash,
                    queryString: "",
                    config: backDropConfig,
                    params: [],
                    queryParams: {}
                });
                return true;
            };

            if (typeof config.owner == 'string') {
                if (!loadBackDrop(config.owner)) {
                    delete config.owner;
                }
            }
            if (typeof config.owner != 'string') {
                 if (!loadBackDrop("")) {
                      router.navigate("");
                      return; // failed
                 }
            }
        }
        var navigatingAway = false;
        var subscription = router.activeInstruction.subscribe(function (newValue) {
            subscription.dispose();
            navigatingAway = true;
            system.acquire(config.moduleId).then(function (dialogInstance) {
                dialog.close(dialogInstance);
            });
        })
        // Have a route. Go back to it after dialog
        var paramInfo = createParams(config.routePattern, fragment, queryString);
        paramInfo.params.unshift(config.moduleId);
        dialog.show.apply(dialog, paramInfo.params)
            .always(function () {
                if (!navigatingAway) {
                    router.navigateBack();
                }
            });
    } else {
        var paramInfo = createParams(config.routePattern, fragment, queryString);
        queueInstruction({
            fragment: fragment,
            queryString: queryString,
            config: config,
            params: paramInfo.params,
            queryParams: paramInfo.queryParams
        });
    }
});

Make sure you import dialog into the module.

Tim
  • 2,968
  • 5
  • 29
  • 55
  • I would love to see an example! It is not clear for me what to override in `loadUrl` as it is basically just matching routes with patterns. – Dziamid Oct 08 '14 at 11:59
  • I will have a go at some point. I've just started to use child routers so its very new to me aswell! – Tim Oct 08 '14 at 15:33
  • Another possibility is you could hook into one of the many router events, like `router:route:activating`, and handle the dialog there. – Brett Oct 08 '14 at 16:16
  • @Brett, I've started a bounty of max possible size, so you are welcome to answer with some code snippets. – Dziamid Oct 08 '14 at 17:27
  • I've had a go at it. It feels good to use in the webapp, but I don't really like how I'v'e had to edit the durandal module itself. – Tim Oct 09 '14 at 10:47
  • Trying to think of an elegant way to hook the route title into the dialogs title. – Tim Oct 09 '14 at 11:01
  • @Tim, this is fantastic! Though I would not recommend fixing vendor code. It is better to create your own router plugin and use it instead of durandal's one. – Dziamid Oct 09 '14 at 11:35
  • @Tim, regarding title, adding `setTitle(config.title);` after `dialog.show` seems to do the trick, doesn't it? – Dziamid Oct 09 '14 at 11:58
  • @Dziamid: Yeah I think it would be best to merge the router and dialog plugin, as your own personal module. When I spoke of setting the title, I meant setting a title inside the modal itself from the router. I use bootstrap modals, so setting the `.modal-header`. – Tim Oct 09 '14 at 13:56
1

Well maybe all of that is not needed when using a trick with the activation data of your home viewmodel. Take a look at my Github repo I created as an answer.

The idea is that the route accepts an optional activation data, which the activate method of your Home VM may check and accordingly show the desired modal.

The benefit this way is that you don't need to touch the existing Durandal plugins or core code at all. I'm though not sure if this fully complies with your request since the requirements didn't specify anything detailed.

UPDATE: Ok I've updated the repo now to work with the additional requirement of generalization. Essentially now we leverage the Pub/Sub mechanism of Durandal inside the shell, or place it wherever else you want. In there we listen for the router nav-complete event. When this happens inspect the instruction set and search for a given keyword. If so then fire the modal. By using the navigation-complete event we ensure additionally that the main VM is properly and fully loaded. For those hacks where you want to navigate to #signin, just reroute them manually to wherever you want.

zewa666
  • 2,593
  • 17
  • 20
  • +1 for the repo, but there's one point you missed. I really want the dialog to `open on top of whatever page there was before`. Your solution would require to fix every potential viewmodel that your can signin from. Can you come up with a more generalized solution? – Dziamid Oct 11 '14 at 08:43
  • I have updated to question to better illustrate the possible use cases, please have a look! – Dziamid Oct 11 '14 at 09:12
  • Looks better. But there's still some problems with your example. I've noticed that `signin` router is tied to `home` route. So, if I were to add route `about`, would I have to also add `about(/:showModal)`? This is what I am trying to avoid. – Dziamid Oct 12 '14 at 13:00
  • Another update with about added ... essentially when you want another route you add just the normal route, but include the optional (/:showModal) parameter. Thus it is able to serve both the modal/non modal view. There is no tying of signin to whatever, because now the router event is observed and just checks the current routes instructions. That way you can reuse the modal for multiple routes – zewa666 Oct 12 '14 at 13:35
  • This is not desirable. What I really looking for is a `#signin` route not tied to any page. Any page can potentially contain it's own optional parameters and should not be aware of Sing-in dialog's modal. – Dziamid Oct 13 '14 at 15:16
  • ok got it ... well the new update solves this with a little trick. We simply don't define the signin route in the router.map sequence. Now we provide a custom mapUnknownRoutes function and check there for the presence of signin fragment. If ok then show the modal. Alternatively we could also listen to the router:route:not-found event and do the same. – zewa666 Oct 13 '14 at 17:30
0

Expanding on my suggestion in the comments, maybe something like this would work. Simply configure an event hook on router:route:activating or one of the other similar events and intercept the activation of /#signin. Then use this hook as a way to display the dialog. Note that this example is for illustrative purposes. I am unable to provide a working example while I'm at work. :/ I can complete it when I get home, but at least this gives you an idea.

router.on('router:route:activating').then(function (instance, instruction) {
  // TODO: Inspect the instruction for the sign in route, then show the sign in 
  // dialog and cancel route navigation.
});
Brett
  • 4,268
  • 1
  • 13
  • 28