durandaldurandal-2.0durandal-navigation

How to use durandal router to activate dialogs?


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).


Solution

  • 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.