alexmingoia / purescript-pux

Build type-safe web apps with PureScript.
https://www.purescript-pux.org
Other
566 stars 76 forks source link

Compose routes and deep nested components #59

Closed jgoux closed 7 years ago

jgoux commented 8 years ago

Hello :hand:,

I'd like to organize my application in a fractal manner. To do so, I need to be able to declare routes locally with their corresponding component. I was able to do it in JS using react + react-router's route object.

Here is an example splitting the current page into components :

image

So we have this hierarchy of components for the current page https://github.com/alexmingoia/purescript-pux/issues/new : Application → Repository → Issues → NewIssue

The idea here is to organize these components according to their relation Parent → Child :

├── Application
│   ├── Repository
│   │   ├── Issues
│   │   │   ├── IssueList.purs
│   │   │   └── NewIssue.purs
│   │   └── Issues.purs
│   └── Repository.purs
└── Application.purs

We can identify "routable" components which display different children based on the current URI. If we imagine that each "routable" component consume a part of the URI as we go deep in the hierarchy we can say that :

In that way, a "routable" component just needs to care about its own part of the URI, and know nothing outside of its direct children.

For the routes definition, we could add a Route data type to each "routable" component definition in addition to the usual State, Action, init, update, view.

-- Application.purs
data Route = Repository

-- Repository.purs
data Route = Issues

-- Issues.purs
data Route = IssueList | NewIssue

This is where I stopped, now I'm looking for a way to declare local match functions, consume and pass the current URI to children so I can repeat the pattern down the hierarchy. :sos:

mrtnbroder commented 8 years ago

I'm missing exactly this.

I'm structuring my components just like @jgoux does, you can see a working example here: https://github.com/mrtnbroder/universal-react-webpack-boilerplate (in js)

take a look at the src/Application folder, where each component exposes it's route, update, view etc. functions and where react-router consumes it at the top level.

This ported to purescript-pux would be awesome!

spencerjanssen commented 8 years ago

Routes could be composed with the Match applicative functor? Pretend I used qualified modules instead of ugly names:

-- App.purs
data AppR
    = WrapRepo RepoR
    | WrapDashboard DashboardR
    | NotFoundR

appMatch :: Match AppR
appMatch =
    WrapRepo <$> repoMatch
    <|> WrapDashboard <$> dashboardMatch
    <|> pure NotFoundR

-- Repo.purs
data RepoR = Repo {owner :: String, name :: String, subroute :: RepoSubR}

data RepoSubR
    = Issue IssueR
    | ViewFile String

repoMatch :: Match RepoR
repoMatch = lit "repo" *> (mk <$> str <*> str <*> (Issue <$> issueMatch <|> fileMatch))
 where
    mk owner name subroute = Repo {owner, name, subroute}
    fileMatch = lit "view-file" *> (ViewFile <$> str)

-- Issue.purs
data IssueR
    = New
    | View Int
    | List

issueMatch :: Match IssueR
issueMatch = lit "issue" *>
    ((lit "new" *> pure New)
    <|> (lit "view" *> (View <$> int))
    <|> (lit "list" *> pure List))

-- Dashboard.purs
data DashboardR = Dashboard -- TODO

dashboardMatch :: Match DashboardR
dashboardMatch = lit "dashboard" *> pure Dashboard
sloosch commented 8 years ago

With inspirations from the react router and ui-router i've come up with the following prototype: Define the routes in a nested structure:

route :: ∀ e. Route AppAction AppState e
route =
    Route "bla" (C.wrapManyWith navigationBar) [
        Route "blub" nestedStatefulRoute [
            Route "here" (const typeHereComp) [],
            Route "here" (const otherComponent) []
        ],
        Route "bar/foo" (const helloComponent) [],
        Route "something" (C.wrapManyWith $ caption "Here is something") [
            Mount $ map implantInApp <<< childRouter
        ]
    ]
....

Every route has a component assigned (Route <path> <component> <children>). The component is responsible to view and update the sub-route components. A component may defines its own route-tree which then can be mounted in any other route (Mount $ map implantInApp <<< childRouter) a.k.a. composition.

Finally a root-router is needed to handle the url-changed event and display/update the current active routes:

main :: Eff _ Unit
main = do
    url <- PuxRouter.sampleUrl
    let rootComp = rootRouter route
    app <- Pux.start {
      initialState : init,
      update: C.update rootComp,
      view : C.view rootComp,
      inputs: [UrlChanged <$> url]
    }
    PuxRouter.navigateTo "bla/blub"
    Pux.renderToDOM "#app" app.html

To make this work we need the concept of a component (update + view function) in pux, this is the same problem as described in #31 (Nesting arbitrary components). So i've reused https://gist.github.com/sloosch/ea98c0c58f9440903c98b952265b556e#file-component-purs

Here is the working prototype in action: test and the full source: https://gist.github.com/sloosch/72c62d30800f1f27a1bc55095c40cd60

Is this what you were asking for? Anyone interested in putting this into a purescript-pux-nested-router? Does anyone know how this is handled in Elm?

Maybe the solution described by spencerjanssen is a better fit. Hadn't time to explore it further to the point where states and actions come into play.

paluh commented 8 years ago

Here is my proof of concept library which tackles this problem from different perspective - alternative (bidirectional - all urls are generated) routing engine:

https://github.com/paluh/purescript-puxing-bob

Please look into examples/multiple-components-routing for working example. Unfortunately view's type became a bit complicated in my approach, because we need to somehow add parent's context to generated urls.

jgoux commented 8 years ago

Thanks all for the answers ! :+1:

I like all the approaches proposed, even if I don't fully understand them (yet). I think @sloosch is my favorite because it's the closest to react-router. :smile: I'd be more than interested to see a purescript-pux-nested-router.

Questions for @sloosch :

Questions for @spencerjanssen :

Question for all :

I also think setting up a little app showing routing capabilities of each solution would be great, so we can compare from a common ground.

alexmingoia commented 7 years ago

I'm archiving most of the discussion threads. If you'd like to continue the discussion I'd prefer if you posted a question on StackOverflow or Gitter. Thanks.