TokamakUI / Tokamak

SwiftUI-compatible framework for building browser apps with WebAssembly and native apps for other platforms
Apache License 2.0
2.58k stars 106 forks source link

Hypothetical Tokamak router module #203

Open MaxDesiatov opened 4 years ago

MaxDesiatov commented 4 years ago

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](https://github.com/pointfreeco/swift-case-paths)?

3. 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.
4. 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.
5. 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.
6. Static websites support? Hard to specify any requirements for that until we have static HTML rendering actually working.
j-f1 commented 4 years ago

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).

carson-katri commented 4 years ago

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...

j-f1 commented 4 years ago

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.

MaxDesiatov commented 4 years ago

@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?

j-f1 commented 4 years ago

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

j-f1 commented 4 years ago

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/

carson-katri commented 4 years ago

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"
MaxDesiatov commented 4 years ago

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

MaxDesiatov commented 4 years ago

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?

carson-katri commented 4 years ago

That's seems like a good idea.

MaxDesiatov commented 4 years ago

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.

carson-katri commented 4 years ago

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.

MaxDesiatov commented 3 years ago

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