Closed ghost closed 9 years ago
There isn't a full router like React Router for Deku. Someone could build one if they wanted to. We treat routes as a data source though. For example, given this url:
/anthonyshort/projects/244242
This route has a user and a project associated with it, and maybe a name:
/:user/projects/:project
This route could be represented by an object:
{
name: 'View Project',
path: '/anthonyshort/projects/244242',
resources: {
user: {
username: 'anthonyshort',
email: anthony@foo.com,
name: 'Anthony Short'
// maybe some more user model stuff here
},
project: {
id: 244242,
name: 'My Project'
}
}
}
We can set this on the app whenever the route changes:
router.on('/:user/projects/:project', function(params){
// use the params to fetch the resources then...
app.set('currentRoute', { ..route object here })
})
And then access the current route in our components using propTypes:
var propTypes = {
'route': { source: 'currentRoute' }
}
function render({ props }) {
let {project,user} = props.route.resources
if (props.route.name === 'View Project') <ProjectView project={project} />
}
If you treat the router like an other external data source it becomes really easy to reason about everything. The route is external state that modifies the way your app renders.
what client side router are you guys using?
We use https://github.com/ianstormtaylor/router, but you can use pretty much anything.
thanks @anthonyshort for your information. And indeed, deku makes things so much simple that we can build our stuffs to work with it :)
I might write a guide that explains how routing can work. This will probably come up a lot.
please, add an example for router, thanks @anthonyshort
I'm curious about the way you "associate" routes with data. Could you be more explicit about this point?
UPD: Oh, nvm, just took a look on router and it became really clear :+1:
ianstormtaylor/router this router cannot installed from npm, so I tried visionmedia/page.js and works well with your suggestions, thank you @anthonyshort
@anthonyshort - It would be cool if you included examples of how to incorporate async data and how to structure nested views in the guide.
For async data, it seems to me that it would be useful to be able to set
promises on the tree, but I assume you guys have come up with another way of dealing with async data.
@anthonyshort :+1:
@anthonyshort how do you handle different layout based pages? For example I'm creating app.tree(<App />)
once but /login
or /notfound
pages have completely different layouts. Should I somehow mount
new tree?
@voronianski we're just in the middle of figuring this out ourselves too :) you probably know more than we do at this point.
We're starting to move toward a single-page app setup, but yeah some of the pages are totally different than others. For that starting to think about this.
First, you should only need 1 deku instance. So been looking at doing something like this:
/**
* Module dependencies.
*/
import {appLoaded} from '../analytics';
import {dom} from 'dekujs/deku';
/**
* Define properties.
*/
export const propTypes = {
mouseOutHandler: { type: 'function', source: 'mouseOutHandler' },
keydownHandler: { type: 'function', source: 'keydownHandler' },
clickHandler: { type: 'function', source: 'clickHandler' },
dialog: { type: 'object', source: 'currentDialog' },
popup: { type: 'object', source: 'currentPopup' },
page: { type: 'object', source: 'currentPage' }
}
/**
* After mount, fire app loaded event.
*/
export function afterMount() {
appLoaded()
}
/**
* Render the "App".
*/
export function render({props}){
let {mouseOutHandler, keydownHandler, clickHandler} = props
let {dialog, popup} = props
let {page} = props
let handlers = {
onMouseOut: mouseOutHandler,
onKeyDown: keydownHandler,
onClick: clickHandler
}
return (
<div class='App' {...handlers}>
<div class='App-body'>{page}</div>
<div class='App-overlays'>
{dialog}
{popup}
</div>
</div>
)
}
That's like the main "layout" or I guess "app". It has a way to handle all the local-to-global components (dialogs, tooltips, popups, etc.), and how to close them, and how to handle keyboard shortcuts and such.
Then you'd use it like this
/**
* Module dependencies.
*/
import App from './app'
/**
* Initialize the app.
*/
deku(<App/>)
.use(components)
.use(actions)
.use(routes)
.use(sources({
currentUser: user
}))
.use(debugging)
.use(rendering)
And the routes would do something like this:
/**
* Module dependencies.
*/
import page from 'visionmedia/page.js'
/**
* Expose `routes`.
*/
export default routes
/**
* Define routes.
*/
function routes(app) {
setup()
function setup() {
page('/', home)
page('/login', login)
page('/notfound', notfound)
}
function login() {
app.set('currentPage', <Login/>)
}
function notfound() {
app.set('currentPage', <NotFound/>)
}
}
And here's some example "actions" used to change routes.
/**
* Module dependencies.
*/
import page from 'visionmedia/page.js'
import * as url from './urls'
/**
* Expose `actions`.
*/
export default actions
/**
* Define actions for app.
*
* These would be used in components like `login: { source: 'visitLogin' }`,
* and called on click of a link, for example.
*/
function actions(app) {
setup()
/**
* Setup routes and route actions.
*/
function setup() {
app.action = app.set // for intuitive dsl, defining actions.
// actions
app.action('visitNotfound', visitNotfound)
app.action('visitLogin', visitLogin)
}
function visitNotfound() {
page(url.notfound())
}
function visitLogin() {
page(url.login())
}
}
Going to be spending more time on this stuff this week, but maybe these initial thoughts spark some ideas.
How does something like that feel so far?
@lancejpollard I think you boilerplate looks good, but I guess @voronianski asked a bit different question.
@voronianski good question, I guess it should be done as a top-level abstraction over deku.
@Kureev oh that demoes a layout system too (assuming no browser refreshes, and that everything is a single-page app).
Basically every totally different page would just be that page
variable in <div class='App-body'>{page}</div>
. Everything else in that component is non-visual, so each page would have full control over how it looks, without requiring separate deku instances, or building an abstraction an top of deku.
With es6 syntax maybe it could be abstracted further :p
route('/notfound', => <NotFound/>)
route('/login', => <Login/>)
function route(path, render) {
page(path, function(){
app.set('currentPage', render())
})
}
But the main thing here is, while these routes just swap out a single component, some more deeply nested routes might change some other data thing (like currentProject
instead of currentPage
), so this probably wouldn't be a general solution.
@lancejpollard thanks for your code examples, they look good :+1:
What about data inside routes? I assume from previous examples that you're getting it somehow similar to this way:
import page from 'page';
import Dashboard from './Dashboard';
export default function (app) {
page('/login', login);
page('/editors', checkAuth, editors);
page('/editors/:id', checkAuth, editorById);
page('/products', checkAuth, products);
page('/products/:id', checkAuth, productById);
page('*', notfound);
page();
function login () {
app.set('currentPage', <Login />);
}
function editors () {
app.set('currentPage', <Dashboard />);
api.getEditors(data => app.set('currentPageData', data));
}
function products () {
app.set('currentPage', <Dashboard />);
api.getProducts(data => app.set('currentPageData', data));
}
function notfound () {
app.set('currentPage', <NotFound />);
}
//...
}
However I'm also thinking that it's possible to treat <Dashboard />
as a container element that will use necessary flux actions (or just plain API calls) in afterMount
to re-render with fetched data, very common pattern in React. What do you think?
Anyway I think the main objective should be to have as much re-renders as possible, for example for shared layouts between different routes.
@voronianski Awesome! For the first part of your question about adding data in routes, there's two main cases at least:
For the first one, we use that when we're on a specific page in our dashboard, and open a modal window with the create form for some resource. That also changes the URL. So it might be like this:
export const propTypes = {
visitEdit: { source: 'visitEditUser' },
user: { type: 'object' }
}
export function render({props}) {
let {visitEdit, user}
return <div onClick={editUser}>Edit</div>
function editUser() {
visitEdit(user)
}
}
app.set('visitEditUser', function(user){
page(`/users/${user.id}/edit`)
})
page('/users/:id/edit', editUser)
function editUser(ctx) {
let userId = ctx.params.id
// try finding from memory so UI loads immediately if it can
let user = api.getUser(userId)
app.set('route', {
resource: 'user',
action: 'edit'
user: user
})
// maybe try experimenting with app.set('editedUser', user), haven't tried yet
}
We then have bindings to our data store to listen for changes, similar to flux but more along the lines of the node-world conventions.
// very generic data binding, basically just listen for 'change'
// on each different collection, and then recompute anything we need,
// normally just a list of stuff, maybe some counts
api.users.on('change', function(){
app.set('users', querySomeUsers()) // fetch fresh users after some change
})
In the above snippets, you'll see that app.set('route', stuff)
. We are using that in a few places. We then inside some page like your <Dashboard/>
, you have a source route: { source: 'route' }
. (Maybe could even get more specific, and have dashboardRoute
and such, haven't experimented with that too much yet). Then you can use that route "data" in some component to change what it renders. Like
let label = 'edit' == action ? 'Update' : 'Create'
Does that answer the first part of your question? For the second part, not quite sure what you're asking:
However I'm also thinking that it's possible to treat
as a container element that will use necessary flux actions (or just plain API calls) in afterMount to re-render with fetched data, very common pattern in React. What do you think? Anyway I think the main objective should be to have as much re-renders as possible, for example for shared layouts between different routes.
Would you mind clarifying and I'll see if I can be of more help :)
The other way to do "routes with data" is to pass the data into your component. Just:
function products () {
api.getProducts(data => app.set('currentPage', <Dashboard data={data} />));
}
But, that would be a blank screen until the data loaded, so for that we've been putting the async call in afterMount
like it sounds like you're saying. There should be a better way tho, just haven't found it yet.
@lancejpollard thanks for the detailed answer.
In my second part question I was talking about smth similar to such example:
// container component..
const Dashboard = {
initialState() {
return { items: [] };
},
afterMount(component, updateState) {
api.getItems(items => updateState({ items });
},
render(component) {
const { state } = component;
if (!state.items.length) {
return <div>Loading...</div>;
}
return (
<div>
<List items={state.items} />
</div>
);
}
};
// ...just renders on specific url
page('/editors', checkAuth, editors);
function editors () {
app.set('currentPage', <Dashboard />);
}
@voronianski Yep! We're doing basically the same thing in a few places :)
I'm basically doing what @anthonyshort initially suggested in a React app using a minimalistic router I put together ('lucid' as in simple): https://github.com/spicydonuts/lucid-router
The router itself is plain JS and just fires a callback when the location changes. It uses the history API and falls back on regular redirects when the route can't be matched. Working it into the app was a one-liner: router.register(location => appState.cursor('location').withMutations(() => location));
Anyway, just started looking at Deku and looking forward to giving it a try!
I am working on a router project, it is not tied to any library or framework but I have designed it with applications based on component trees in mind: https://github.com/router5/router5 (project is early stage, website needs to be created).
I have plans for integration with React and Deku. I am currently working on integration with React (Mixins and Link component).
I have played with Deku and router5 together and it showed promise. I don't have a full picture on how deku internals work, especially environment data (sources) and plugins. If anyone is interested and/or has the vision for a router5-deku integration, you are more than welcome to help!
@troch Looks like a cool project. I might steal your nested route syntax :D
@spicydonuts Be my guest :). I have exploded it in specialised modules on purpose: path-parser for building / matching paths, and route-node for building a tree of named routes.
Cool, I'll have a look at those too. I basically wrapped url-pattern in an html5 history-aware wrapper, with fallbacks. He's doing a pretty good job with that project, with a refactor coming up that uses an AST to make the matching code simpler while also letting you go backwards (generate urls from a route name and params). I might bring up nested patterns there because it'd be a lot easier to do at the AST level. The only way I could do it is by flattening the routes, which would just make it an easier syntax for multiple routes that share a root path.
I looked at url-pattern and route-parser, and another source of inspiration was routington.
I needed:
url-pattern and route-parser don't have those three, that's why I came up with my own path parser / tokeniser. Routington is not bad but parses paths by "/" rather than existing routes.
Thinks are progressing. A website is live: router5.github.io. I can now fully focus on inegration with React and Deku.
I wonder what would be the best approach with Deku. My initial thoughts are: if I use source
through app.set('currentRoute', {...})
, it won't do any good for nested routes. I would then need to nest trees. Other approach is to do sideways updates using router5.addNodeListener(node, fn)
which is triggered if re-rendering needs to happen on a specific node. Does anyone have an opinion?
For info router5 is in rc and router5-deku in alpha.
@troch This is great. I'd love to help out more with this. You could probably use sources for now, but we're adding support for context and the ability to mutate the component tree in the next version. I'll need to look more at your implementation for it / the docs to know how we could help expose some better APIs for things like this.
@anthonyshort Thanks. I have used source for links, so they can update their active state.
For route changes and updating views / components, source is only useful if you have 1 level deep routes (and it is perfectly possible with the decorator I provide: just don't pass a listener to it -or don't use it at all- and set currentRoute as a prop from source).
But if you have more than one level deep routes, then you need to only target the deepest common node between your previous and current route, and you don't want source to cause unnecessary renders.
Just to let you know, I have released router5@1.0.0. As part of the release, deku-router5 has been completely reworked (and renamed, previously router5-deku). It makes available a plugin, a HOC (for route nodes) and a link/button component.
@troch Nice! Checking it out now.
I'm looking for an example of router in deku. It would be nice if anyone can share it :dancer:
I'm excited with Deku :+1: