Skip to content

belas 2

pakx edited this page Aug 10, 2024 · 1 revision

Basic Elm-Like App Structure (BELAS)

This is PART II of the article on Basic Elm-Like App Structure (BELAS).

PART II: routing

Basic Routing

Let's look at handling routing with the app structure we've built. Our strategy will be to represent changes in the route as changes in the model, then to continue as before by using that model to render a view.

Specifically, we'll use m.route() and a RouteResolver, in routeResolver.onmatch we'll call a method of the actions object (we consider route-changes "outside input"), the actions object will update model, which will then be rendered by view().

As an example let's create a small app which will display a "FRUITS" or "VEGETABLES" pane depending on whether we're on a "/fruits" or "/vegetables" route. We'll use the same index.html from PART I. Here's app.js:

code listing: routing
// app.js

window.addEventListener("DOMContentLoaded", main)

function main() {
    var settings = getSettings()
        , model = createModel(settings)
        , actions = createActions(model)
        , view = createView()
        , routeResolver = createRouteResolver(model, actions, view)
        , defaultRoute = "/" + settings.paths[0]

    m.route(document.body, defaultRoute, routeResolver)
}

function getSettings() {
    return {
        paths : ["fruits", "vegetables"]
    }
}

function createModel(settings) {
    return {
        paths: settings.paths.slice()
        , routeName: undefined
    }
}

function createActions(model) {
    return {
        onNavigateTo: onNavigateTo
    }

    function onNavigateTo(routeName, params) {
        model.routeName = routeName
    }
}

function createRouteResolver(model, actions, view) {
    return {
        "/fruits": {
            onmatch: function(params, route) {
                actions.onNavigateTo("fruits", params)
            }
            , render: function() {
                return view(model, actions)
            }
        }
        , "/vegetables": {
            onmatch: function(params, route) {
                actions.onNavigateTo("vegetables", params)
            }
            , render: function() {
                return view(model, actions)
            }
        }
    }
}

function createView() {

    return vwApp

    function vwApp(model, actions) {
        return m(".app"
            , vwHeader(model)
            , model.routeName == "fruits"
                ? vwFruits(model)
                : vwVegetables(model)
        )
    }

    function vwHeader(model) {
        return m(".nav"
            , model.paths.map(function(itm) {
                return [
                    m("a"
                        , {href: "/" + itm, oncreate: m.route.link}
                        , itm
                    )
                    , " "
                ]
            })
        )
    }

    function vwFruits(model) {
        return m(".fruits", "FRUITS" )
    }

    function vwVegetables(model) {
        return m(".vegetables", "VEGETABLES")
    }
}

Here's a Flems of this code.

Routing + Params

What if our routes also had route- and maybe even query-params? As a contrived example let's say the /fruits and /vegetables routes have :name and :color respectively as optional route params, and qty in each case as a query param. Here's app.js:

Also, while we hand-crafted a RouteResolver above, this time we "generate" one. If we have unique route names this is a "pattern" we can use with all routed apps.

code listing: routing + params
// app.js

window.addEventListener("DOMContentLoaded", main)

function main() {
    var settings = getSettings()
        , model = createModel(settings)
        , actions = createActions(model)
        , view = createView()
        , routeResolver = createRouteResolver(model, actions, view)
        , defaultRoute = settings.defaultRoute

    m.route(document.body, defaultRoute, routeResolver)
}

function getSettings() {
    var routes = [
            { name: "fruits", route: "/fruits" }
            , { name: "fruits-name", route: "/fruits/:name" }
            , { name: "vegetables", route: "/vegetables" }
            , { name: "vegetables-color", route: "/vegetables/:color" }
        ]
        , settings = {
            routes: routes
            , defaultRoute: routes[0].route + "?qty=1"
        }
    return settings
}

function createModel(settings) {
    return {
        routes: settings.routes.slice()
        , routeName: undefined
        , params: undefined
    }
}

function createActions(model) {
    return {
        onNavigateTo
    }

    function onNavigateTo(routeName, params) {
        model.routeName = routeName
        model.params = params
    }
}

function createRouteResolver(model, actions, view) {
    return model.routes.reduce(function(acc, itm) {
        acc[itm.route] = {
            onmatch: function(params, route) {
                actions.onNavigateTo(itm.name, params)
            }
            , render: function() {
                return view(model, actions)
            }
        }
        return acc
    }, {})
}

function createView() {

    return vwApp

    function vwApp(model, actions) {
        return m(".app"
            , vwHeader(model)
            , model.routeName.indexOf("fruits") > -1
                ? vwFruits(model)
                : vwVegetables(model)
        )
    }

    function vwHeader(model) {
        var sampleParams = {
            "fruits-name" : "/fruits/apple"
            , "vegetables-color" : "/vegetables/green"
        }

        return m(".nav"
            , model.routes.map(function(itm, idx) {
                var href = (sampleParams[itm.name] || ("/" + itm.name))
                        + "?qty=" + (idx + 1)
                return [
                    m("a"
                        , {href: href, oncreate: m.route.link}
                        , itm.name
                    )
                    , " "
                ]
            })
        )
    }

    function vwFruits(model) {
        return m(".fruits"
            , "FRUITS"
            , m("", JSON.stringify(model.params))
        )
    }

    function vwVegetables(model) {
        return m(".vegetables"
            , "VEGETABLES"
            , m("", JSON.stringify(model.params))
        )
    }
}

Here's a Flems of this code.

Touchpoints

Points to keep in mind:

  • syncing route-to-model starts with routeResolver-onmatch() calling actions.onNavigateTo(...) with arguments. What we pass as arguments depends on our routes and needs -- in our examples above it suffices to pass a "route name" since for us they're unique. The rule of thumb is to pass in something, even an artificial, generated value, that lets onNavigateTo() uniquely identify the route, and thereby figure out expected params and set model state.

BTW, while it's possible for actions.onNavigateTo() to get route information by calling m.route.get() directly, we pass in that information so as to keep our Mithril-aware code, here createRouteResolver, cohesive and decoupled from our Mithril-agnostic code, here actions.onNavigateTo().


Continue to PART III

Contents

Resources

Clone this wiki locally