New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SSR Supporting #2

Closed
wants to merge 3 commits into
base: master
from
Jump to file or symbol
Failed to load files and symbols.
+2,237 −154
Diff settings

Always

Just for now

@@ -13,10 +13,13 @@
},
"dependencies": {
"@ragg/fleur": "*",
"routr": "2.1.0"
"history": "4.7.2",
"path-to-regexp": "2.4.0"
},
"devDependencies": {
"@types/history": "4.7.0",
"jest": "23.6.0",
"ts-jest": "23.10.4"
"ts-jest": "23.10.4",
"typescript": "3.0.3"
}
}
@@ -0,0 +1,24 @@
import * as React from 'react'
import { withComponentContext, ContextProp } from '@ragg/fleur-react'
import { createBrowserHistory, History, LocationListener } from 'history'
import { navigateOperation } from '@ragg/fleur-route-store/src/navigateOperation';
export const HistoryHandler = withComponentContext(class HistoryHandler extends React.Component<ContextProp> {
private history: History
public componentDidMount() {
this.history = createBrowserHistory({})
this.history.listen(this.handleChangeLocation)
}
private handleChangeLocation: LocationListener = (location) => {
this.props.context.executeOperation(navigateOperation, {
url: location.pathname,
})
}
public render() {
return null
}
})
@@ -0,0 +1,35 @@
import * as React from 'react'
import { withComponentContext, ContextProp } from '@ragg/fleur-react';
import { navigateOperation } from '@ragg/fleur-route-store/src/navigateOperation'
import { parse } from 'url'
type Props = ContextProp & React.AnchorHTMLAttributes<HTMLAnchorElement>
export const Link = withComponentContext(class Link extends React.Component<Props> {
render() {
const { context, onClick, ...rest } = this.props
return React.createElement('a', [], { ...rest, onClick: this.handleClick })
}
private isRoutable = () => {
const { href } = this.props
const parsed = parse(href || '')
const current = parse(location.href)
if (!href) return false
if (href[0] === '#') return false
if (parsed.protocol && parsed.protocol !== current.protocol) return false
if (parsed.host && parsed.host !== location.host) return false
return true
}
private handleClick = (e: React.MouseEvent) => {
if (!this.isRoutable()) return
e.preventDefault()
this.props.context.executeOperation(navigateOperation, {
url: parse(this.props.href!).pathname!,
})
}
})
@@ -0,0 +1,55 @@
import RouteStore from './RouteStore'
import { withStaticRoutes } from './index';
describe('RouteStore', () => {
let store: RouteStore<any>
beforeEach(() => {
const StaticRouteStore = withStaticRoutes({
articlesShow: {
path: '/articles/:id',
handler: null,
}
})
store = new StaticRouteStore()
})
it('Should correct make path', () => {
const path = store.makePath('articlesShow', { id: 1 })
expect(path).toBe('/articles/1')
})
it('Should correct make path with params', () => {
const path = store.makePath('articlesShow', { id: 1 }, { comment: 1 })
expect(path).toBe('/articles/1?comment=1')
})
it('Should get route with hash / query', () => {
const route = store.getRoute('/articles/1?a=1#anchor')
expect(route).toEqual({
name: 'articlesShow',
url: '/articles/1?a=1',
params: { id: '1' },
query: { a: '1' },
config: {
path: '/articles/:id',
handler: null,
}
})
})
it('Should get route without hash / query', () => {
const route = store.getRoute('/articles/1')
expect(route).toEqual({
name: 'articlesShow',
url: '/articles/1',
params: { id: '1' },
query: {},
config: {
path: '/articles/:id',
handler: null,
}
})
})
})
@@ -1,4 +1,8 @@
import { listen, Store } from '@ragg/fleur'
import * as pathToRegexp from 'path-to-regexp'
import * as url from 'url'
import * as qs from 'querystring'
import { navigateFailure, navigateStart, navigateSuccess, NavigationPayload } from './actions'
import { MatchedRoute, RouteDefinitions } from './types'
@@ -17,13 +21,12 @@ export default class RouteStore<R extends RouteDefinitions> extends Store<State>
isComplete: false,
}
protected router: any
protected routes: RouteDefinitions
// @ts-ignore
private handleNavigateStart = listen(navigateStart, ({ url, method }: NavigationPayload) => {
private handleNavigateStart = listen(navigateStart, ({ url }: NavigationPayload) => {
const currentRoute = this.state.currentRoute || { name: null }
const nextRoute = this.matchRoute(url, { method })
const nextRoute = this.matchRoute(url)
if (nextRoute && nextRoute.name === currentRoute.name) {
return
@@ -44,23 +47,28 @@ export default class RouteStore<R extends RouteDefinitions> extends Store<State>
})
// @ts-ignore
private handleNavigationFailure = listen(navigateFailure, ({ error }: NavigationPayload) => {
private handleNavigationFailure = listen(navigateFailure, ({ error }: NavigationPayload) => {
this.updateWith(draft => {
draft.error = error || null
draft.isComplete = true
})
})
public rehydrate(state: State) {
this.updateWith(draft => Object.assign(draft, state))
this.updateWith(draft => {
Object.assign(draft, state)
draft.currentRoute = state.currentRoute ? this.getRoute(state.currentRoute.url) : null
})
}
public dehydrate() {
public dehydrate(): State {
return this.state
}
public makePath(routeName: keyof R, params: object = {}, query: object = {}): string {
return this.router.makePath(routeName, params, query)
const path = this.routes[routeName as string].path
const pathname = pathToRegexp.compile(path)(params)
return url.format({ pathname: pathname, query })
}
public getCurrentRoute(): MatchedRoute | null {
@@ -75,8 +83,8 @@ export default class RouteStore<R extends RouteDefinitions> extends Store<State>
return this.state.isComplete
}
public getRoute(url: string, options: { method: string }) {
return this.matchRoute(url, options)
public getRoute(url: string): MatchedRoute | null {
return this.matchRoute(url)
}
public getRoutes() {
@@ -88,19 +96,32 @@ export default class RouteStore<R extends RouteDefinitions> extends Store<State>
return !!(currentRoute && currentRoute.url === href)
}
private matchRoute(url: string, options: any): MatchedRoute | null {
const indexOfHash = url.indexOf('#')
const urlWithoutHash = indexOfHash !== -1 ? url.slice(indexOfHash) : url
const route = this.router.getRoute(urlWithoutHash, options)
if (!route) return null
return {
name: route.name,
url: route.url,
params: route.params,
query: route.query,
...route.config,
private matchRoute(inputUrl: string): MatchedRoute | null {
const indexOfHash = inputUrl.indexOf('#')
const urlWithoutHash = indexOfHash !== -1 ? inputUrl.slice(0, indexOfHash) : inputUrl
const parsed = url.parse(urlWithoutHash)
const params = Object.create(null)
for (const routeName of Object.keys(this.routes)) {
const keys: pathToRegexp.Key[] = []
const matcher = pathToRegexp(this.routes[routeName].path, keys)
const match = matcher.exec(parsed.pathname!)
if (!match) continue
const route = this.routes[routeName]
for (let idx = 1; idx < match.length; idx++) {
params[keys[idx - 1].name] = match[idx]
}
return {
name: routeName,
url: urlWithoutHash,
params,
query: qs.parse(parsed.query!),
config: route,
}
}
return null
}
}
@@ -2,7 +2,6 @@ import { action } from '@ragg/fleur'
export interface NavigationPayload {
url: string,
method: string
error?: Error
}

This file was deleted.

Oops, something went wrong.
@@ -1,5 +1,4 @@
import Fleur from '@ragg/fleur'
import * as Router from 'routr'
import { navigateOperation, withStaticRoutes } from './index'
describe('test', () => {
@@ -10,15 +9,15 @@ describe('test', () => {
meta: {
requireAuthorized: false,
},
action: () => {}
action: () => { }
},
articles_show: {
articlesShow: {
path: '/articles/:id',
handler: 'ArticleShowHandler',
meta: {
requireAuthorized: true,
},
action: () => {}
action: () => { }
},
error: {
path: '/error',
@@ -36,30 +35,24 @@ describe('test', () => {
const context = app.createContext()
it('Should route to correct handler', async () => {
await context.executeOperation(navigateOperation, { url: '/articles', method: 'GET' })
await context.executeOperation(navigateOperation, { url: '/articles' })
const route = context.getStore(RouteStore).getCurrentRoute()
expect(route.handler).toBe('ArticleHandler')
expect(route.config.handler).toBe('ArticleHandler')
})
it('Should route to correct handler with ', async () => {
await context.executeOperation(navigateOperation, { url: '/articles/1', method: 'GET' })
await context.executeOperation(navigateOperation, { url: '/articles/1' })
const route = context.getStore(RouteStore).getCurrentRoute()
expect(route.handler).toBe('ArticleShowHandler')
expect(route.config.handler).toBe('ArticleShowHandler')
expect(route.params.id).toBe('1')
})
it('Should handle exception ', async () => {
await context.executeOperation(navigateOperation, { url: '/error', method: 'GET' })
await context.executeOperation(navigateOperation, { url: '/error' })
const error = context.getStore(RouteStore).getCurrentNavigateError()
console.log(error)
expect(error).toMatchObject({ message: 'damn.', statusCode: 500 })
})
it('Make path from routes', () => {
const path = context.getStore(RouteStore).makePath('articles_show', { id: 1 })
expect(path).toBe('/articles/1')
})
})
@@ -1,52 +1,21 @@
import { operation, OperationContext } from '@ragg/fleur'
import * as Router from 'routr'
import { navigateFailure, navigateStart, navigateSuccess } from './actions'
import RouteStore from './RouteStore'
import { MatchedRoute, RouteDefinitions } from './types'
export const navigateOperation = operation(async (context: OperationContext<any>, { url, method }: { url: string, method: string }) => {
const routeStore = context.getStore(RouteStore)
context.dispatch(navigateStart, { url, method })
const route = routeStore.getCurrentRoute()
if (!route) {
context.dispatch(navigateFailure, {
url,
method,
error: Object.assign(new Error(`URL ${url} not found in any routes`), { statusCode: 404 })
})
return
}
try {
if (route.action) {
await Promise.resolve(route.action(context, route))
}
context.dispatch(navigateSuccess, { url, method })
} catch (e) {
context.dispatch(navigateFailure, { url, method, error: Object.assign(e, { statusCode: 500 }) })
}
})
import { RouteDefinitions } from './types'
export const withStaticRoutes = <R extends RouteDefinitions>(routes: R): {
storeName: string
new (...args: any[]): RouteStore<R>
new(...args: any[]): RouteStore<R>
} => {
const router = new Router(routes)
return class StaticRouteStore extends RouteStore<R> {
public static storeName = 'fleur-route-store/RouteStore'
constructor() {
super()
this.router = router
this.routes = routes
}
}
}
export { RouteStore, MatchedRoute as Route }
export { navigateOperation } from './navigateOperation'
export { RouteStore, RouteDefinitions }
export { MatchedRoute } from './types'
export { Link } from './Link'
Oops, something went wrong.
ProTip! Use n and p to navigate between commits in a pull request.