Open Tobbe opened 3 years ago
In case everyone was wondering what Rails does... ;)
Optional path parts are surrounded with parenthesis:
get 'users(/:username)' => :index, :as => :users
So that responds to both /users
and /users/cannikin
Once one part is optional, everything after that has to be optional as well, you can't have an optional in the middle and something required after that.
You can also nest the optionals, so you can define multiple possible routes all within one string:
get 'posts(/:slug(/comments))' => :index, :as => :posts
Which responds to /posts
, /posts/hello-world
and /posts/hello-world/comments
. Note that in that example, the second optional isn't even a variable, it's a fixed string that has to be present. So if you tried /posts/hello-world/foobar
that would not match.
The Rails router also has a splat that says "take the rest of the parts of the URL and put them all in a variable named X", like:
get 'posts/*rest' => :index, :as => :posts
Which means that a URL like /posts/hello-world/comments/123
(or any URL that starts with /posts
) goes to the PostsController, with a param named rest
that contains the string hello-world/comments/123
. You can then parse that string manually and decide what to do.
As for making sure that the variable is one of a selection, the Rails routes can accept a constraints
parameter that the variable has to match against in order to consider the route a match:
`get '/comments/:id' => :show, :constraints => { :id => /\d+/ }`
The constraint can also be a function ("block" in Ruby lingo) so you can do more complex processing and return true/false.
@mojombo and I talked about all of this when we were first building the router, but it was way too much to worry about when it was just starting out, so we started with the basics. If we had this functionality though it would definitely allow for maximum flexibility when defining your routes. And adding this syntax doesn't complicate the simple cases at all, you just keep doing what you're doing!
Thanks for your input @cannikin. This validates the idea I think. We're not crazy for wanting optional parts of the routes 🙂
But so, how would the Rails router solve @simoncrypta's use case with the language as an optional parameter at the start of the URL?
Once one part is optional, everything after that has to be optional as well, you can't have an optional in the middle and something required after that.
So for Simon's case every route would have to consist of only optional paths? That'd be weird 😬
Thanks for your input @cannikin. This validates the idea I think. We're not crazy for wanting optional parts of the routes 🙂
But so, how would the Rails router solve @simoncrypta's use case with the language as an optional parameter at the start of the URL?
Once one part is optional, everything after that has to be optional as well, you can't have an optional in the middle and something required after that.
So for Simon's case every route would have to consist of only optional paths? That'd be weird 😬
That question me how they do i18n routing in Rails. I checked on the web some example and I found this one interesting They don't use the same method that @cannikin show us, but they give us a good hint for the use case with the language :
"To make the :locale parameter optional, you can pass in a regular expression with the available routes as in the code below"
# config/routes.rb
Rails.application.routes.draw do
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
scope "(:locale)", locale: /en|ja/ do
resources :users, only: [:new, :show]
root 'welcome#index'
#... more routes
end
end
There's another part to the Rails syntax I forgot to include! You can define defaults for variables if they're not present:
get 'photos(/:filter)', to: 'photos#show', defaults: { filter: 'all' }
That one has the default on the end though, and since we want an optional first part I think you'd need two routes to encompass all the possibilities:
get ':language/about', to: 'pages#about'
get 'about', to: 'pages#about', defaults: { language: 'jpg' }
But that means you'd need two routes for each and every possible other route, ugh.
To in this situation, Rails has a scope
directive which can surround a group of routes and acts as a prefix to all of them:
scope "(:locale)", constraints: { locale: /#{I18n.available_locales.join("|")}/ }, defaults: {locale: "en"} do
get 'about', to: 'pages#about'
end
Note that this one does double duty, checking that the locale is one of a list defined in I18n.available_locales
and if not, defaulting to "en"
.
So that responds to:
/about
(and behind the scenes sets the incoming language
param to "en"
as if it was present in the URL/en/about
/fr/about
I know we ideally want a completely flat routes file but then it makes it much more difficult to do stuff like @simoncrypta wants to do...
@simoncrypta posted his response just SECONDS before I hit send on mine! Great minds think alike! 😄
There's another part to the Rails syntax I forgot to include! You can define defaults for variables if they're not present:
We could add that functionality too, to our user route param types
const userRouteParamTypes = {
lang: {
constraint: /en|fr|sv|de/,
default: 'fr',
}
}
Any updaets?
The simplest way would be to create two rules:
const Routes = () => {
return (
<Router>
<Route path="/user/{id:Int}" page={UserPage} name="user" />
<Route path="/user/{id:Int}/{prop:String}" page={UserPage} name="user" />
</Router>
)
}
Then in page:
interface Props {
id: number
opt?: string
}
const UserPage = ({ id, opt }: Props) => {
return (
<>
<MetaTags title="User" description="User page" />
{(opt && <p>User page with some optional parameter</p>) || (
<p>User page</p>
)}
</>
)
}
To expound on @ubugnu 's answer, you must place the shorter route last so that any URL generation later on in your app does not explode:
const Routes = () => {
return (
<Router>
<Route path="/user/{id:Int}/{prop:String}" page={UserPage} name="user" />
<Route path="/user/{id:Int}" page={UserPage} name="user" />
</Router>
)
}
Also leaving my approach here: https://community.redwoodjs.com/t/implementing-internationalization-with-redwood-js/5004/6?u=anaclumos
Optional Path Parameters
Existing functionality
This is what you do to have a path parameter named
id
You can add a constraint and a "type cast" by doing this, and you'll get a
number
instead of astring
when you use itYou can even add custom types
Rationale
@simoncrypta mentioned to me he would like to have a parameter that optionally defines the language to use, so you can have all routes that start with e.g. /fr/ be in French, everything that start's with /de/ be in German etc.
Proposed functionality
I propose we add support for optional parameters by appending a question mark (
?
) to the end of the param name.In usage it would look like this
If you navigated to /de/about you'd get the About page in German, if you navigated to /about you'd get it in whatever default language was configured.
Challenges
The biggest challenge is probably detecting if an optional parameter was passed or not. Like if we just had this
When someone goes to /about, is it HomePage with
opt
set to "about", or is it About page withopt
undefined? It might also not handle 404s as nicely as it would otherwise. If someone goes to /aboot currently that would be a 404. Now it would be a valid navigation to the Home page, withopt
set to "aboot"