Separating route data from your AppViewModel


#1

In done-autorender 2.3.0 it’s now easy to separate your route data from the rest of your Application ViewModel. This means no more serialize: false noise in your property definitions. This article will go over how you can upgrade your app to take advantage of this separation of concerns.

Routing changes in DoneJS

A pair of new features in DoneJS has made routing easier to implement. In CanJS we now have constructible can-components and can-value that makes it easier to route to the correct component without a lot of switch statements in the template. The routing guide goes over this pattern as well as others (such as setting up routing rules within the ViewModel).

DoneJS apps use done-autorender which requires a slightly different way to do routing. We’re in the process of bringing as many of the new routing ideas from CanJS into DoneJS as we can.

route.data defaults to a DefineMap

In most apps the process of wiring up route params to the Application ViewModel has been straight-forward, but quite tedious. You typically need to define the params in two places like so:

const AppViewModel = DefineMap.extend("AppViewModel", {
  page: "string",
  id: "number"
});

route.register("{page}", { page: "home" });
route.register("{page}/{id}", { page: "product", id: 1 });

Notice that the above:

Requires defining two properties, page and id both in the DefineMap and in the routing rules.
The types are defined in the DefineMap but the defaults are defined in the routing rules.

Since all of the information is already contained within the routing rules we thought why not just make route.data be a DefineMap, with a page and id property defined.

If you upgrade to can-route 4.4.x that’s exactly what you get. In order to get the page you can simply get: route.data.page.

This leads to a nice separation between your root ViewModel and the route data. In CanJS with automount you can define a Component like so:

Component.extend({
  tag: "app-home",

  ViewModel: {
    routeData: {
      default: () => {
        route.register("{page}", { page: "home" });
        route.start();
        return route.data;
      }
    },
    User: UserModel
  },

  view: `
   <h1>Welcome to {{routeData.page}}</h1>
   <h2>User {{user.name}}</h2>
  `
});

This is much nicer. We don’t need to specify properties on the ViewModel to not be serialized to the URL, nor do we need to define a route observable type.

Specifying the route.data in done-autorender

The above change is nice, but in order to take advantage of it for DoneJS users we needed support in done-autorender. In done-autorender 2.3.0 it’s now possible to specify a property on your Application ViewModel to use as the route data, rather than using the ViewModel itself.

Specify the route-data attribute in your viewModel’s can-import statement like so:

<can-import from="todo-list/app" export-as="viewModel" route-data="routeData" />

The above says:

  1. Use the todo-list/app module as this template’s ViewModel.
    1 Use the routeData property on that ViewModel to connect to route.data.

This property can of course be named whatever you wish, we are using routeData in documentation for clarity.

Defining your Application ViewModel just means setting it’s routeData property to the default route.data:

import DefineMap from "can-define/map/map";
import route from "can-route";
import RoutePushstate from "can-route-pushstate";

route.urlData = new RoutePushstate();
route.register("{page}", { page: "home" });

const AppViewModel = DefineMap.extend("AppViewModel", {
	routeData: {
		default: () => route.data
	}
});

export default AppViewModel;

If you need to use properties within the route, such as the page in this case, just specify the routeData.page property. Here’s how that looks in your template:

<h1>Todo App</h1>
<h2>Page {{routeData.page}}</h2>

Upgrading your app

You can take advantage of these features simply:

  1. Upgrade to at least can-route 4.4.0 and done-autorender 2.3.0.
  2. Remove any properties from your AppViewModel that are specific to the route (anything that is defined in your routing rules).
  3. Add a property for your route data, we call it routeData. It should contain only a default export that is route.data.
  4. Update your index.stache and add the route-data="routeData" attribute to your viewModel’s can-import.
  5. Change any references in the template, such as bindings and magic tags, to use routeData.foo instead of just foo.
  6. The fun part: get rid of all of the serialize: false noise from your AppViewModel and never need to add it again.

That’s it! Even with a large app this change should take no more than an hour or so.

Looking ahead

Routing is an area that we continue to refine in DoneJS and CanJS. In DoneJS 3.0 we will bring over more of the patterns laid out in the Routing Guide. In the future we might unify the automount pattern with done-autorender somehow. The DoneJS Survey currently contains a proposal for simplifying routing configuration. We are always looking for feedback on how we can make routing easier, so please fill out the survey.


#2

Cool, thanks for this post.
Is this approach applicable to CanJS@3 applications ?
I suspect that updaiting can-route to v4 is bad idea. What if I add same routeData property to appVM and add same logic to value ?


#3

Why wasn’t routeData separate from viewModel to begin with?

Was there a perceived benefit (organizational or otherwise) to having it on the viewModel, or was it just that it was convenient and easy at the time?


#4

@Lighttree the route-data feature is only available in done-autorender 2.x which works with canjs 4.x. It might be possible to cherry-pick that commit down to done-autorender 1.x. If you did that you could create your own routeData property that is a DefineMap, you just couldn’t use the default route.data that is only in can-route 4. If you wanted to try backporting the feature to done-autorender 1.x i’d happily accept a PR.


#5

@leoj3n done-autorender wasn’t really meant to solve routing, it just sort of had to be involved because of SSR. Ideally we would like to evolve donejs to adopt what canjs is now suggesting you do: have an automounted top-level component and handle your routing in that component, yourself.


#6

Simple apps that need routing often have no other top-level state. So it makes sense in these apps to have the route data just be the app vm.


#9

Thanks for clarifying that in certain cases we may still want to serialize properties on the app vm for routing.


#10

As you described in the OP, isn’t that what this code already enables? What else needs to be done?

Is the following the related issue we should be watching?

I guess I don’t fully understand what is “automount”, and how it’s not fully unified with done-autorender yet.

Can you explain that? Thanks.


#11

automount just means putting your root component directly in your .html file. For example:

<html>
<head>
  <title>Some title</title>
</head>
<my-app></my-app>

<script src="node_modules/steal/steal.js" main></script>
</html>