Router - view canDeactivate=false - disable other elements


#1

If I have a view/viewmodel where my canDeacativate() method is returning false, is there an easy way to disable all other elements on-screen?

My actual use-case is fairly complex - I have a number of individual router-views on a dashboard screen - each with a read view and edit view being composed in based on the master/detail example from Matthew James Davis’s blog - http://davismj.me/blog/master-detail/ - all working nicely.

But when I have one of the edit views active, I need to disable all the other views, navigation menu, etc.

Right now, I am doing it with just setting an overlay div with lower z-index than the active editing view (when the edit view is not saved/cancelled). Works, but a bit nasty. I was wondering if there was some way to just tap into the router state directly and have some kind global-ish way (custom attribute or something) that could disable every button, from element, link etc (other than those on the active editing view).


#2

Thanks for the love @stevies.

I’m not sure where canDeactivate() fits in.

Your best bet with this construction might be to add a router event handler that sets a property in the containing shell view model. Use the property to read a class into the parent element, e.g. editing, and finally use some CSS to get your effect.

.editing .view-page {
  position: relative;

  &::after {
     content: "";
     position: absolute;
     top: 0; right: 0; bottom: 0; left: 0;
     background-color: rgba(0,0,0,0.5);
     pointer-events: none;
  }

You might also consider using compose. Take a look at this article if you haven’t seen it already, maybe one of the best articles every written on Aurelia, by @zewa666: https://www.sitepoint.com/composition-aurelia-report-builder/


#3

Thanks for the reply.

I am using canDeactivate() to prevent the route changing if the form is dirty when the user tries to leave the form (by clicking on something else in the view). That works fine.

The problem I am having it how to easily disable all the other elements, buttons, links, etc, on screen when the open form has unsaved data. OK - these elements won’t work if they are triggering a navigation - all clicks and button presses will get swallowed by the canDeactivate on the active route view returning false. But to the user, it looks like the app is broken with clickable links and buttons looking active but just appearing to do nothing. And I probably also want to prevent clicking stuff that is not part of the route navigation.

The overlay approach does not really solve those problems 100%, and is a bit of a no-no from an accessibility point of view. I probably do need to disable things properly and add aria tags etc.

You mention a router event handler. I think that is what I was alluding to. What event would it be that I would need to subscribe to - I can’t see any documentation on the internals of the router and events it is broadcasting?

Also, my problem would be that every single widget in my app would need to have its disabled property aware of that event (in addition to any other rules that would make it disabled). That can get messy and become a bit of a maintenance nightmare.

I am guessing I would need something that could do this for anything listening (effectively every widget on the view):

  • user is trying to change the route
  • but that has been blocked by some existing vm’s canDeactivate() stepping in and blocking it
  • I need to be disabled too (if I am not already disabled by something else)

Originally, I was just routing from the read view to a completely separate edit view. I then tried sliding it in like in your demo. But the designers are keen to do editing in place all on the dashboard view by appearing to flip the card states from read to edit. I did not have to change too much to do that - but I do end up with a lot of <router-view></router-view> on my main dashboard page.

I could rewrite it to use composition easily enough, but that does not appear to give me any advantage (maybe I am missing something) - I still have lots of stuff on the page that needs to be disabled when any given panel is in edit mode. And there is no navigation happening, so no canDeactivateto block the card state change - I would need to manage what read/edit view was being composed or visible at any given time. Not a huge problem doing that.

My screenshots above are a really cut down example. The real app could have 10s of read/edit cards on screen at any one time.

Off course there is always another option. Don’t try to block all the screen widgets, but just alert the user if they try to leave an open edit card with with unsaved form data. That is much easier to code, but less nice for the user because we allow them to do things that result in errors and warnings.

I have a demo project at:

where I am trying to explore some of the issues.


#4

So there are multiple questions burried here in your request. Lets see if we can get that sorted. I’m referring to the dashboard page of your example.

  1. Router events
    You can find a nice list of router-events which you can listen using the EventAggregator. Take a look at this article
    router:navigation:canceled is likely one of them you’d be interested.

  2. Trapping focus
    The section 3 view seems to have a custom trap focus attribute which is a nice way of handling it. Alternatively try using something like
    https://github.com/davidtheclark/focus-trap and wrapping that, which handles a lot of use cases.
    Your main issue though is that you’re attaching your listener to the section itself instead of the body. Over there at the body you’d need to capture tabs as well and re-set them back to your section.


Now some architectural questions:

If I understood it properly you want to

  • If a section toggles into edit mode
  • limit tabs and activity only to this specific section
  • don’t let the user navigate away so he can’t loose modifications
  • once saved/canceled release the lock

The complications are that we want to achieve above workflow for every widget/section without having to hardcode it into each of them itself. So what I’d do is the following:

  • Have a global state handler (service + EventAggregator, aurelia store, whatever) to keep track of the section in edit mode. E.g isInEditMode: boolean
  • Every edit button from sections sets the sections element ( @inject(Element) ) as, lets call it, activeSectionElement: HTMLElement.
  • only the dashboard view, instead of every section, has a trap-focus handler which
    • is active if isInEditMode is true
    • listens on the whole document.body but limits tab-cycles and clicks to the to activeSectionElement to circumvent the issues you described as label of Section 3 by cancelling outside clicks and re-placing them back into activeSectionElements first child.
    • has an on-change handler to react to changing activeSectionElements
  • every section does
    • keep track of the local dirty state
    • share a common canDeactivate pattern: export a function which can be imported in every one of them or use inheritance with a basesection implementing the default canDeactivate. As for composability I’d recommend going with the first approach.

Now since we talked a lot about state I obviously have to point out the Aurelia Store :wink:
Take a look at @kennyfowler’s interesting sample here that was an example to drive this feature. The store plugin itself has all sorts of benefits of solving complex app scenarios as the above.

  • Keeping track of everything in one place
  • Handling permissions (canToggle/save) globally via Middlewares
  • Acting/toggling based on state via subscriptions (e.g TrapFocus mereley would need to pluck for a sub-state and with its own subscription could handle work independently)
  • Undo/Redo for free which is often the next request in examples like yours :wink:

The sample itself is really cool as it shows of some really interesting concepts of Aurelia and a more complex scenario compared to classic Todo apps. Lets see how this evolves


#5

Many thanks for taking the time to reply with such a detailed response.

The list of router events was exactly what I was looking for. I am guessing router:navigation:canceled will fire - I’ll check that out and see what happens. Thanks.

I am already using Aurelia Store extensively in my real app so using that to keep track of the active tabs state should be really easy. I will add the Store to my demo spike app and take it from there. I really should remember to always add Aurelia Store to every application - even quick demos - because it so useful.


#6

Update. Turns out that the focus-trap library is much more fully-featured and flexible than my own basic attempt at doing similar. I can use this to do everything I want and it makes most of the other techniques I was attempting with the router bit redundant.

Aurelia makes it really easy to use that in a custom attribute that you can add to any container that you want to trap focus inside.