Tokamak icon indicating copy to clipboard operation
Tokamak copied to clipboard

Hypothetical Tokamak router module

Open MaxDesiatov opened this issue 3 years ago • 13 comments

I had a look at SwiftWebUI Router and it's great stuff, thank you for developing this @carson-katri! I hope it could be used with Tokamak with slight modifications.

In the meantime, I'm just spitballing some requirements for a new router API that I hope could be designed and implemented at some point in the future:

  1. Ideally it would be compatible with SwiftUI, i.e. you recompile your code for Apple platforms and this router package is able to handle deep links (and maybe even user activities for Handoff too?) seamlessly.
  2. I'm not a fan of routes specified as strings. I wonder if an API could be created that's statically typed, but still provides enough flexibility, maybe something like this?
enum AppRoute: Codable {
  case orders
  case orderDetails(id: Int)
  case admin(AdminRoute)
}

enum AdminRoute: Codable {
  case users
  case userDetails(id: Int)
}

The complication here is that Codable doesn't play well with enums well out of the box. Maybe an alternative could be built (doesn't have to rely on Codable actually) with something like CasePaths?

  1. It should be modular and composable, so that an external package (say an admin user-management package akin to the example above) could be integrated into any app and its routing tree without assuming absolute paths. E.g. I deploy the app to example.com/app, the app itself ideally shouldn't assume absolute paths to allow that deployment, but also if it integrates the user management package URLs like example.com/app/admin/users should work seamlessly too.
  2. Similarly, integrating resources should also compose without any absolute path assumptions in light of https://github.com/swiftwasm/Tokamak/pull/155 and https://github.com/swiftwasm/carton/issues/38 so that packages/targets that declare resoruces with same names don't experience URL clashes. This one's probably harder, but just as important I think.
  3. I'm not sure how it should integrate with the new lifecycle App and Scene types, I think routes belong to views first, but I might be wrong. Actual prototype code might be needed to clarify this.
  4. Static websites support? Hard to specify any requirements for that until we have static HTML rendering actually working.

MaxDesiatov avatar Jul 22 '20 20:07 MaxDesiatov

Something to consider is whether the API should be top-level (specify your routes all at once in some sort of structure inside App or Scene) or more organic (like React Router which lets you have multiple routers in different parts of the app).

j-f1 avatar Jul 22 '20 20:07 j-f1

I’ve actually experimented with static rendering and got something up and working fairly quickly. I can make a PR if that’d be of interest...

carson-katri avatar Jul 22 '20 20:07 carson-katri

Static websites support? Hard to specify any requirements for that until we have static HTML rendering actually working.

It would be pretty useful if there was a way to statically export all the valid routes as HTML files/serverless functions like Gatsby or Next.js do so it could be uploaded to a static host and have proper server-side 404s.

j-f1 avatar Jul 22 '20 20:07 j-f1

@j-f1 yes, I think I'd prefer the latter React-Router-inspired approach, otherwise I'm not sure how to make relative URL composability work from points 3. and 4. I mentioned in the issue description.

It would be pretty useful if there was a way to statically export all the valid routes as HTML files/serverless functions like Gatsby or Next.js do so it could be uploaded to a static host and have proper server-side 404s.

Totally, I'd imagine the renderer would traverse all views while generating HTML anyway, it could then record all the routes and split the generated HTML into separate files for separate routes at later stages of the rendering pipeline.

I’ve actually experimented with static rendering and got something up and working fairly quickly. I can make a PR if that’d be of interest...

@carson-katri I'm very interested! I want tokamak.dev to become a proper website with docs and demos and a landing page, all built with Tokamak as much as possible, maybe we could pre-render your TokamakDocs app with that?

MaxDesiatov avatar Jul 22 '20 20:07 MaxDesiatov

Inspiration: Django, React Router, Vue Router, SwiftUI (based on old react router api), Flask, React Navigation (react native)

j-f1 avatar Jul 22 '20 20:07 j-f1

Some info on how to handle deep links in SwiftUI: https://medium.com/better-programming/deep-links-universal-links-and-the-swiftui-app-life-cycle-e98e38bcef6e, https://nalexn.github.io/swiftui-deep-linking/

j-f1 avatar Aug 22 '20 14:08 j-f1

I actually have an almost working type-safe Router package. I'm still working on the SwiftUI implementation, and then it should be able to support Tokamak after that (although it relies on PreferenceKey which I don't quite have working in the toolbar branch). Here's what it'd look like:

// Routes.swift
enum AppRoutes: Routes {
  case orders
  case orderDetails(id: Int, OrderRoutes?)
  static let defaultRoute: Self = .orders
}

enum OrderRoutes: Routes {
  case overview
  case bill
  static let defaultRoute: Self = .overview
}

// ContentView.swift
struct ContentView : View {
  var body: some View {
    Router(AppRoutes.self) {
      Route(AppRoutes.orders) {
        List(Order.sampleData) {
          RouterLink($0.name, to: AppRoutes.orderDetails(id: $0.id, .overview)) // Link to a specific Route
        }
      }
      Route(AppRoutes.orderDetails) { order in // enum cases can be used as functions
        Router(OrderRoutes.self) { // Sub Routers
          if case let .orderDetails(id, _) = order {
            Route(OrderRoutes.overview) { OverviewView(id: id) }
            Route(OrderRoutes.bill) { BillView(id: id) }
          }
        }
      }
    }
  }
}

It has a RouteEncoder and RouteDecoder, so you can navigate directly to a route from a String:

AppRoutes.orderDetails(id: 0, .overview) <-> "orderDetails/0/overview"

Any Codable type (besides Collections ATM) can also be used in a route string:

struct Todo: Codable {
  let id: Int
  let task: String
}
TodoRoutes.todo(.init(id: 0, task: "Pick up dinner")) <-> "todo/0/Pick%20up%20dinner"

carson-katri avatar Aug 22 '20 15:08 carson-katri

As far as I understand, it would satisfy all the requirements from the original post, this is amazing! 👏

MaxDesiatov avatar Aug 22 '20 19:08 MaxDesiatov

I wonder if it would make sense as a separate package in a separate repository so that SwiftUI people could use it without adding a dependency on Tokamak? It would need something like

#if canImport(TokamakShim)
import TokamakShim
#elseif canImport(SwiftUI)
import SwiftUI
#else
#error("Add a dependency on the Tokamak repository")
#endif

to allow linking it all without a dependency on Tokamak on Apple platforms (target dependency conditions in Package.swift won't help because https://github.com/apple/swift-package-manager/pull/2749 was merged after SwiftPM 5.3 was branched off).

I have a separate TokamakUI organization ready, maybe it's time to move it all there together with TokamakDocs, so that we don't pollute the SwiftWasm organization?

MaxDesiatov avatar Aug 22 '20 19:08 MaxDesiatov

That's seems like a good idea.

carson-katri avatar Aug 22 '20 19:08 carson-katri

Please feel free to create a separate repository under you account in the meantime. I'll transfer the main Tokamak repo to that org and set up the permissions later next week when I get access to my Mac again, and you can transfer yours if you'd like to do so at all when you're ready.

MaxDesiatov avatar Aug 22 '20 19:08 MaxDesiatov

I've got the code up at carson-katri/router. There are definitely improvements to be made, so feel free to open issues/PRs there now. Also, it only supports SwiftUI ATM.

carson-katri avatar Aug 24 '20 00:08 carson-katri

Just stumbled upon this routing code from the parsing library by the Point-Free folks, seems like an interesting alternative to Codable...

MaxDesiatov avatar Dec 29 '20 15:12 MaxDesiatov