Dziamid
Dziamid

Reputation: 11579

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

Upvotes: 1

Views: 989

Answers (3)

zewa666
zewa666

Reputation: 2603

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.

Upvotes: 1

Brett
Brett

Reputation: 4269

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

Upvotes: 0

Tim
Tim

Reputation: 3038

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.

Upvotes: 2

Related Questions