Giter VIP home page Giter VIP logo

Comments (20)

chinedufn avatar chinedufn commented on May 14, 2024

So I've been adding things as I've needed them. Haven't needed a router yet since I've only been building a home page, but I certainly will soon.

Funny enough yesterday I started googling around to see what routers already exist (without much luck).

I'm still thinking about this, but super curious if you already have any ideas about how routing should look?

Regardless this can be the tracking issue for routing (whether we implement it in percy or find an existing standalone router implementation that we can just show example usage of in the examples dir)

from percy.

qm3ster avatar qm3ster commented on May 14, 2024

I'd look at rocket to see what currently exists for routing.
Setting components for routes seems like the right idea. If a route should display different components, it can just be a component wrapping that selection.
Route and query parameters are kind of a nontrivial issue - how do we provide them to the components in a typed fashion? Who depends on whomst'd've in the end, router on page component or page component on router?

from percy.

chinedufn avatar chinedufn commented on May 14, 2024

Very, very interesting, will definitely take a look at rocket's routing. In general probably need to look at a few different good routers, even if they aren't only in Rust. Sweet!


So in a site that I'm working on I've been experimenting with how aRust frontend web app could be organized.

One thing that I'm toying with at the moment is just having a single State struct with only private fields.

image

I pass this State into my views and they grab what they need.


I also pass in ViewUtils, a set of common utilities that my views need, such as getting URLs for different resources.


I haven't used this pattern enough to know if it scales well or not, but one thing I have in mind here... just providing a starting point for what I'm about to say:


So in this scenario.. I'm imagining that when you visit a route the router parses the URL / query string into a Params struct (probably auto generated via a macro but could start by manually defining it)

struct Params {
  cat_count: Option<u32>.
  cat_type: Option<String>
}

Params are stored in state (or maybe view utilities.. idk..)

// In my view:
let cat_count = state.params().cat_count.unwrap();
let html = html! { <div> { *cat_count } </div> };

And on page load as well as anytime you changed routes (clicked an anchor tag or changed programmatically) we'd update the Params struct accordingly by matching against the Router.

A pattern that I've been using lately in frontend apps has been state.msg(Msg::DoSomething)

Example:

image

So I could imagine that when you clicked an anchor tag or to change programmatically it'd just be state.msg(Msg::Route("/cats/black_cat/45/") and the handler for that would user the Router on that route string to generate the new state.params

Granted this is off the top of my head so I'd imagine that I'm missing a ton of considerations!

Curious about your thoughts?

from percy.

qm3ster avatar qm3ster commented on May 14, 2024

The two options I see:

  1. Yeah, whatever state management structure is used should include the parsed or unparsed route as a property. Changing it here should lead to navigation, and the initial page load should set it.
  2. Routing should be separate from the state management structure, in a middleware that runs on the server during first load or the client during client navigation. The middleware can reject the navigation with an error, cause an internal redirect, or set some state, eg from route params.
    There can even be asynchronous middleware that makes eg network requests before navigation.

The first way is simpler and will always keep in sync.
The second way is more flexible and powerful.

from percy.

chinedufn avatar chinedufn commented on May 14, 2024

Oooo yeah option 2 would be be incredibly useful for stuff like admin routes.

Until I read up on Rocket and the other projects with powerful routers my idea bank here will be weak -> very curious about if you already have a sense of where a starting point would be for something like option 2?

How might option 2 look as a first implementation? What about as something fully featured? Trying to get a better picture in my head.

But yeah I'll read up on a few popular backend routers that sounds so much better on paper!

from percy.

chinedufn avatar chinedufn commented on May 14, 2024

Did some reading up on rocket’s routing and boy is that awesome.

We can take a lot of inspiration from that for sure.

I’m traveling this next week and a half so not sure of the exact timing... but I’m going to get some notes together one what a first implementation of a view router might look like and share them here.

from percy.

chinedufn avatar chinedufn commented on May 14, 2024

Starting sketching out how this might look here -> https://chinedufn.github.io/percy/router/index.html

Feeling about 60% of the way there.. doesn't feel quite ergonomic yet but close.. nailing down the details..

But even still your feedback on the general direction / look and feel would be highly appreciated!

from percy.

qm3ster avatar qm3ster commented on May 14, 2024

Looks really good.
Saw a typo,

#[route(path = "/posts/{post_id/edit", before = IsAdmin)]

should be

#[route(path = "/posts/{post_id}/edit", before = IsAdmin)]

not sure I like {} vs <> delimiters, they're a bit overloaded, since they're string interpolation in format! and inside html!, maybe /posts/:post_id/edit

from percy.

chinedufn avatar chinedufn commented on May 14, 2024

Ah thanks for catching that typo

And I like the :post_id good idea 👍🏿

from percy.

chinedufn avatar chinedufn commented on May 14, 2024

Here's a breakdown of how the procedural macro for #[route(path = ..., before = isAdmin, ... etc)] could be built.. Courtesy of David Tolnay -> dtolnay/syn#516

from percy.

cryptoquick avatar cryptoquick commented on May 14, 2024

I'm experimenting with this awesome project to build a full stack Rust app, and I have to say, great work so far.

I just wanted to ask, is the current implementation supposed to use the history push API to alter the location path? If not, are there plans for this?

Also, I had to submodule the repo into my project because a few of the isomorphic app example's modules weren't published yet.

I understand the project is still in an early state, but I have to say, great work so far, you have my encouragement, and if you're looking for donations to continue your work here, I'm sure there are ways to arrange that with OpenCollective or Patreon or similar.

from percy.

chinedufn avatar chinedufn commented on May 14, 2024

@cryptoquick thanks for all of those very kind words - so, so kind!!

And thanks a lot for all of the contributions that you made today!


Yeah - the routing story is incomplete. I'm using Percy for the website for a game that I'm working on so whenever I'm working on that website I end up working on Percy but whenever I'm working on gameplay progress slows a bit.

So - the story here is that I haven't sat down to dig into routing quite yet. What you see so far was more of an initial exploration.

WRT the history API. You're spot on that the isomorphic app example doesn't currently use it. This is only because I quickly added the routing part in a hotel room at the Rust Belt Rust conference and didn't add in the history API. I do plan to beef up the example with that.


In general you're making me realize that it would be useful to lay out a roadmap for Percy so that people can see what's coming up and are also more empowered to contribute!


Sorry about some of those modules not being published yet. If it helps you can use git url's for dependencies so that you don't need to add them as submodules.

# In your Cargo.toml
router-rs = { git = "https://github.com/chinedufn/percy }

Also - please continue to feel very free to open issues because that helps with prioritizing what to do. Absolutely pumped that you're experimenting with using Percy for a full stack Rust app and I'd love to do whatever is possible to make that experimentation easier!

from percy.

chinedufn avatar chinedufn commented on May 14, 2024

Alright I'm diving into the router. Here's the test case that I'm currently working on.

image

Will try and get a PR out ASAP.


WRT to the history API in the isomorphic example - I'll get that updated in the PR as well.


Thanks for the nudge @cryptoquick !

from percy.

cryptoquick avatar cryptoquick commented on May 14, 2024

Very cool! Also, I did something that I'm not super happy with, but I wanted it for type-correctness and consistency as well as just editor hints, and that was to use this following format for routes with the current router:

use router_rs::prelude::*;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use virtual_dom_rs::prelude::*;

use crate::store::*;
use crate::views::*;

// TODO: Is there a better way to do this? To deduplicate this?

pub enum ActivePage {
    Home,
    Management,
    Contractors,
    Report,
}

pub fn get_path(page: &ActivePage) -> &'static str {
    match page {
        ActivePage::Home => "/",
        ActivePage::Management => "/management",
        ActivePage::Contractors => "/contractors",
        ActivePage::Report => "/report",
    }
}

pub fn get_page(path: &str) -> ActivePage {
    match path {
        "/" => ActivePage::Home,
        "/management" => ActivePage::Management,
        "/contractors" => ActivePage::Contractors,
        "/report" => ActivePage::Report,
        &_ => ActivePage::Home,
    }
}

pub fn make_router(store: Rc<RefCell<Store>>) -> Router {
    let mut router = Router::default();

    let store_clone = Rc::clone(&store);
    let param_types = HashMap::new();
    let home_route = Route::new(
        "/",
        param_types,
        Box::new(move |_params| Box::new(HomeView::new(Rc::clone(&store_clone))) as Box<View>),
    );

    let store_clone = Rc::clone(&store);
    let param_types = HashMap::new();
    let contractors_route = Route::new(
        "/contractors",
        param_types,
        Box::new(move |_params| {
            Box::new(ContractorsView::new(Rc::clone(&store_clone))) as Box<View>
        }),
    );

    let store_clone = Rc::clone(&store);
    let param_types = HashMap::new();
    let management_route = Route::new(
        "/management",
        param_types,
        Box::new(move |_params| {
            Box::new(ManagementView::new(Rc::clone(&store_clone))) as Box<View>
        }),
    );

    let store_clone = Rc::clone(&store);
    let param_types = HashMap::new();
    let report_route = Route::new(
        "/report",
        param_types,
        Box::new(move |_params| Box::new(ReportView::new(Rc::clone(&store_clone))) as Box<View>),
    );

    router.add_route(home_route);
    router.add_route(management_route);
    router.add_route(contractors_route);
    router.add_route(report_route);

    router
}

This code isn't ideal, since each route is duplicated in three places, but hopefully you see where I'm going with it. Also, just know, this isn't terribly open source code, but I'm providing it as an example of what an application developer might desire when working with Rust, and expecting their editor and their compiler from making mistakes with routes. Also, I'm not sure how it would work if I wanted to provide parameters to routes, like in your case. It might be interesting to experiment with user-implemented router methods, perhaps. The desire is for something that feels Rust-idiomatic, without a lot of repetition, and also, provides the route parameter flexibility desired in the near future.

Feel free to break the API, that's fine when using software like this, and I already have the repo submoduled in so I can test changes right away. I'm super happy you're involved and working on this. There's a lot of good energy here that isn't in yew and other projects. I consider this to be best-in-class if you want to build full-stack Rust.

BTW, I would suggest changing the phrasing, "isomorphic", since it's been brought up a couple of times on its overuse and lack of descriptiveness, and I even was really tired the other day and called it "isometric". I think "Full Stack Rust" is a much better way to describe the thing that's being worked on here. The only problem will come when it comes time to deploy, since there's little serverless support out there. It'd be sort of interesting to tackle serverless Rust with V8 Isolates like CloudFlare is doing, but they have their own fetch interface, which obviates much of what's already built for backend modules in the Rust ecosystem that relies on lower-level OS-facing modules like reqwest and things like that.

from percy.

chinedufn avatar chinedufn commented on May 14, 2024

@cryptoquick thanks for sharing! It's super useful to see how you're using it so that I can factor real use cases into the design!!


I have tests passing for route parameters right now - here's how it looks.

#![feature(proc_macro_hygiene)]

use router_rs::prelude::*;
use router_rs_macro::{create_routes, route};
use virtual_node::{VirtualNode,VText};

// No Params

#[route(path = "/")]
fn no_params() -> VirtualNode {
    VirtualNode::Text(VText::new("hello world"))
}

#[test]
fn root_path() {
    let mut router = Router::default();

    router.set_route_handlers(create_routes![no_params]);

    assert_eq!(
        router.view("/").unwrap(),
        VirtualNode::Text(VText::new("hello world"))
    );
}

// Route With One Param

#[route(path = "/:id")]
fn route_one_param(id: u32) -> VirtualNode {
    VirtualNode::Text(VText::new(format!("{}", id).as_str()))
}

#[test]
fn one_param() {
    let mut router = Router::default();

    router.set_route_handlers(create_routes![route_one_param]);

    assert_eq!(
        router.view("/10").unwrap(),
        VirtualNode::Text(VText::new("10"))
    );
}

// Route With Two Params

#[route(path = "/user/:user_id/buddies/:buddy_id")]
fn route_two_params(user_id: u64, buddy_id: u64) -> VirtualNode {
    VirtualNode::Text(VText::new(format!("User {}. Buddy {}", user_id, buddy_id).as_str()))
}

#[test]
fn two_params() {
    let mut router = Router::default();

    router.set_route_handlers(create_routes![route_two_params]);

    assert_eq!(
        router.view("/user/50/buddies/90").unwrap(),
        VirtualNode::Text(VText::new("User 50. Buddy 90"))
    );
}

The next test case that I'm implementing (doesn't work yet) looks like this - and I think will solve what you're mentioning about providing data. It's inspired by Rocket's managed state.

// Route with Provided Data
struct State {
    count: u8
}

#[route(path = "/")]
fn route_provided_data(state: ProvidedData<State>) -> VirtualNode {
    VirtualNode::Text(VText::new(format!("Count: {}", state.count).as_str()))
}

#[test]
fn provided_data() {
    let mut router = Router::default();

    router.provide(State {count: 50});

    router.set_route_handlers(create_routes![route_provided_data]);

    assert_eq!(
        router.view("/").unwrap(),
        VirtualNode::Text(VText::new("Count: 50"))
    );
}

WRT to the name isomorphic - I've opened #93 to address that

from percy.

cryptoquick avatar cryptoquick commented on May 14, 2024

Very cool. Let me know the code's available so I can refactor.

Also, another thing comes to mind: The state management pattern is starting to become a little odious in my app. It's very much like a naive but idiomatic Redux implementation, and although there is redux-rs, I think it might be more valuable to make a Percy-specific React Hooks implementation; specifically, for useState, useReducer, and useEffect. I'm considering taking that on. I might make an issue for that, if you think that'd be good.

from percy.

chinedufn avatar chinedufn commented on May 14, 2024

I haven't used react hooks so I'm a bit ignorant to the value prop (did some quick reading just now).

In general I have a fear of introducing concepts that people need to learn in order to be productive.

In general I've gotten a lot of mileage out of React (we use it at my day job) by pretty much ignoring lots of the bells and whistles in the ecosystem and just having one global state object that gets passed around into every component (along with any smaller stuff that a parent component might choose to pass to a chid).

The simplicity of this makes it easy to be productive without needing to learn concepts that get churned when new concepts get popular.

That being said I haven't used hooks so I'm certainly curious to hear more about what you think could be of use here!


So yeah - I'm always open to better ideas! Feel free to open an issue explaining the problems that you're running into and how you think we can solve them!!!

from percy.

chinedufn avatar chinedufn commented on May 14, 2024

Alright I've got things working nicely. You can have unlimited route parameters and unlimited state.

Here's a screenshot with one route parameter and one state type to illustrate how it's used.

image

Now I'll need to clean up the code and update the example ( that I will no longer refer to as isomorphic (; )

from percy.

cryptoquick avatar cryptoquick commented on May 14, 2024

Neat! Set up a branch & PR, I'm eager to try it out!

from percy.

chinedufn avatar chinedufn commented on May 14, 2024

@cryptoquick mind giving #95 a quick read over? Once that's merged routing should be much nicer!

from percy.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.