├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── apps ├── create-router.js ├── cycle │ ├── README.md │ ├── components │ │ ├── Compose.js │ │ ├── Inbox.js │ │ ├── InboxList.js │ │ ├── Main.js │ │ ├── Message.js │ │ └── Nav.js │ ├── create-router.js │ ├── data │ │ └── emails.js │ ├── main.js │ ├── router5 │ │ ├── driver.js │ │ ├── link-interceptor-plugin.js │ │ ├── link-on-click.js │ │ └── router-to-observable.js │ └── routes.js ├── cycle2 │ ├── components │ │ ├── Nav.js │ │ └── Route.js │ ├── create-router.js │ ├── main.js │ └── routes.js ├── deku-redux │ ├── actions │ │ └── draft.js │ ├── api.js │ ├── components │ │ ├── App.js │ │ ├── Compose.js │ │ ├── Inbox.js │ │ ├── InboxItem.js │ │ ├── InboxList.js │ │ ├── Link.js │ │ ├── Main.js │ │ ├── Message.js │ │ ├── Nav.js │ │ └── NotFound.js │ ├── main.js │ ├── reducers │ │ ├── draft.js │ │ └── emails.js │ └── store.js ├── deku │ ├── api.js │ ├── components │ │ ├── App.js │ │ ├── Compose.js │ │ ├── Inbox.js │ │ ├── InboxItem.js │ │ ├── InboxList.js │ │ ├── Main.js │ │ ├── Message.js │ │ ├── Nav.js │ │ └── NotFound.js │ └── main.js ├── react-redux │ ├── actions │ │ └── draft.js │ ├── components │ │ ├── App.js │ │ ├── Compose.js │ │ ├── Inbox.js │ │ ├── InboxItem.js │ │ ├── InboxList.js │ │ ├── Link.js │ │ ├── Main.js │ │ ├── Message.js │ │ ├── Nav.js │ │ └── NotFound.js │ ├── main.js │ ├── reducers │ │ ├── draft.js │ │ └── emails.js │ └── store.js ├── react │ ├── api.js │ ├── components │ │ ├── App.js │ │ ├── Compose.js │ │ ├── Inbox.js │ │ ├── InboxItem.js │ │ ├── InboxList.js │ │ ├── Main.js │ │ ├── Message.js │ │ ├── Nav.js │ │ └── NotFound.js │ └── main.js └── routes.js ├── example2.css ├── index.html ├── package.json ├── scripts └── buildAll.sh ├── styles.css ├── styles2.css └── webpack.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | 9 | charset = utf-8 10 | end_of_line = lf 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 router5 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Moved to router5 repository](https://github.com/router5/router5/tree/master/packages/examples) 2 | -------------------------------------------------------------------------------- /apps/create-router.js: -------------------------------------------------------------------------------- 1 | import createRouter from 'router5'; 2 | import loggerPlugin from 'router5/plugins/logger'; 3 | import listenersPlugin from 'router5/plugins/listeners'; 4 | import browserPlugin from 'router5/plugins/browser'; 5 | import routes from './routes'; 6 | 7 | export default function configureRouter(useListenersPlugin = false) { 8 | const router = createRouter(routes, { 9 | defaultRoute: 'inbox' 10 | }) 11 | // Plugins 12 | .usePlugin(loggerPlugin) 13 | .usePlugin(browserPlugin({ 14 | useHash: true 15 | })); 16 | 17 | if (useListenersPlugin) { 18 | router.usePlugin(listenersPlugin()); 19 | } 20 | 21 | return router; 22 | } 23 | -------------------------------------------------------------------------------- /apps/cycle/README.md: -------------------------------------------------------------------------------- 1 | # Cycle and router5 2 | 3 | This is development work for a new router5 cycle driver. 4 | 5 | ## Build 6 | 7 | From the root directory this repo: 8 | 9 | ```sh 10 | npm run build -- --app cycle 11 | http-server -p 8080 12 | ``` 13 | 14 | Navigate to http://localhost:8080. 15 | 16 | -------------------------------------------------------------------------------- /apps/cycle/components/Compose.js: -------------------------------------------------------------------------------- 1 | import { h, div, h4, input, textarea, p } from '@cycle/dom'; 2 | import Rx from 'rx'; 3 | 4 | function Compose(sources) { 5 | const initialState = { title: '', message: '' }; 6 | 7 | const title$ = sources.DOM.select('.mail-title') 8 | .events('input') 9 | .map(evt => evt.target.value); 10 | 11 | const message$ = sources.DOM.select('.mail-message') 12 | .events('input') 13 | .map(evt => evt.target.value); 14 | 15 | const messageAndTitle$ = Rx.Observable.combineLatest( 16 | title$.startWith(''), 17 | message$.startWith(''), 18 | (title, message) => ({ title, message }) 19 | ); 20 | 21 | const compose$ = Rx.Observable.combineLatest( 22 | messageAndTitle$, 23 | sources.router.error$ 24 | .filter(err => err && err.code === 'CANNOT_DEACTIVATE') 25 | .startWith(''), 26 | ({ title, message }, routerError) => div({ className: 'compose' }, [ 27 | h4('Compose a new message'), 28 | input({ className: 'mail-title', name: 'title', value: title }), 29 | textarea({ className: 'mail-message', name: 'message', value: message }), 30 | routerError ? p('Clear inputs before continuing') : null 31 | ]) 32 | ); 33 | 34 | return { 35 | DOM: compose$, 36 | router: messageAndTitle$ 37 | .map(({ message, title }) => [ 'canDeactivate', 'compose', !message && !title ]) 38 | .distinctUntilChanged(_ => _[2]) 39 | }; 40 | }; 41 | 42 | export default Compose; 43 | -------------------------------------------------------------------------------- /apps/cycle/components/Inbox.js: -------------------------------------------------------------------------------- 1 | import Rx from 'rx'; 2 | import { h, div } from '@cycle/dom'; 3 | import InboxList from './InboxList'; 4 | import Message from './Message'; 5 | 6 | function Inbox(sources) { 7 | const emails$ = Rx.Observable.combineLatest( 8 | sources.router.routeNode$('inbox'), 9 | sources.data.emails$, 10 | (route, emails) => { 11 | const email = route.name === 'inbox.message' 12 | ? emails.find(({ id }) => id === route.params.id) 13 | : null; 14 | 15 | return { emails, email }; 16 | } 17 | ); 18 | 19 | const inbox$ = emails$ 20 | .map(({ emails, email }) => { 21 | const inboxList = InboxList({ emails, buildUrl: sources.router.buildUrl }); 22 | const message = email && Message({ email }); 23 | 24 | return div({ className: 'inbox' }, [ 25 | inboxList, 26 | message 27 | ]); 28 | }); 29 | 30 | return { 31 | DOM: inbox$ 32 | }; 33 | }; 34 | 35 | export default Inbox; 36 | -------------------------------------------------------------------------------- /apps/cycle/components/InboxList.js: -------------------------------------------------------------------------------- 1 | import { ul, li, a, h4, p } from '@cycle/dom'; 2 | 3 | function InboxList({ emails, buildUrl }) { 4 | return ul({ className: 'mail-list' }, [ 5 | emails.map(({ id, mailTitle, mailMessage }) => li( 6 | a({ href: buildUrl('inbox.message', { id }) }, [ 7 | h4(mailTitle), 8 | p(mailMessage) 9 | ]) 10 | )) 11 | ]); 12 | } 13 | 14 | export default InboxList; 15 | -------------------------------------------------------------------------------- /apps/cycle/components/Main.js: -------------------------------------------------------------------------------- 1 | import Rx from 'rx'; 2 | import { h, div, a, makeDOMDriver } from '@cycle/dom'; 3 | import { startsWithSegment } from 'router5.helpers'; 4 | import Inbox from './Inbox'; 5 | import Compose from './Compose'; 6 | 7 | function Main(sources) { 8 | const routerSource = sources.router; 9 | 10 | const routeComponent$ = routerSource 11 | .routeNode$('') 12 | .map(route => { 13 | const startsWith = startsWithSegment(route); 14 | 15 | if (startsWith('inbox')) { 16 | return Inbox(sources); 17 | } 18 | 19 | if (startsWith('compose')) { 20 | return Compose(sources); 21 | } 22 | 23 | return { 24 | DOM: Rx.Observable.of(div('Route component not implemented')) 25 | }; 26 | }); 27 | 28 | return { 29 | DOM: routeComponent$ 30 | .flatMapLatest(component => component.DOM), 31 | router: routeComponent$ 32 | .flatMapLatest(component => component.router || Rx.Observable.empty()) 33 | }; 34 | }; 35 | 36 | export default Main; 37 | -------------------------------------------------------------------------------- /apps/cycle/components/Message.js: -------------------------------------------------------------------------------- 1 | import { h, h4, p } from '@cycle/dom'; 2 | 3 | function Message({ email }) { 4 | return h('section', { className: 'mail' }, [ 5 | h4(email.mailTitle), 6 | p(email.mailMessage) 7 | ]); 8 | } 9 | 10 | export default Message; 11 | -------------------------------------------------------------------------------- /apps/cycle/components/Nav.js: -------------------------------------------------------------------------------- 1 | import { h, a } from '@cycle/dom'; 2 | 3 | function Nav(sources) { 4 | const routerSource = sources.router; 5 | 6 | const nav$ = routerSource 7 | .route$ 8 | .map(route => 9 | h('nav', [ 10 | a({ href: sources.router.buildUrl('inbox'), className: routerSource.isActive('inbox') ? 'active' : '' }, 'Inbox'), 11 | a({ href: sources.router.buildUrl('compose'), className: routerSource.isActive('compose') ? 'active' : '' }, 'Compose') 12 | ]) 13 | ); 14 | 15 | return { 16 | DOM: nav$ 17 | }; 18 | }; 19 | 20 | export default Nav; 21 | -------------------------------------------------------------------------------- /apps/cycle/create-router.js: -------------------------------------------------------------------------------- 1 | import createRouter, { loggerPlugin } from 'router5'; 2 | import browserPlugin from 'router5/plugins/browser'; 3 | 4 | const configureRouter = (routes) => { 5 | return createRouter(routes, { 6 | defaultRoute: 'inbox' 7 | }) 8 | .usePlugin(loggerPlugin) 9 | .usePlugin(browserPlugin({ 10 | useHash: true 11 | })); 12 | }; 13 | 14 | export default configureRouter; 15 | -------------------------------------------------------------------------------- /apps/cycle/data/emails.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | "id": "1", 4 | "mailTitle": "Why router5?", 5 | "mailMessage": "I imagine a lot of developers who will first see router5 will ask themselves the question: is it yet another router? is it any good? Why oh why do people keep writing new routers all the time? It is not always easy to see the potential of something straight away, or understand the motivations behind. I therefore decided to try to tell you more about router5, why I decided to develop an entire new routing solution, and what problems it tries to solve." 6 | }, 7 | { 8 | "id": "2", 9 | "mailTitle": "Use with Cycle", 10 | "mailMessage": "Make a driver using your router instance, observe route and node changes and request navigation" 11 | }, 12 | { 13 | "id": "3", 14 | "mailTitle": "Compose a new message", 15 | "mailMessage": "Click on compose, start to fill title and message fields and then try to navigate away by clicking on app links, or by using the back button." 16 | } 17 | ]; 18 | -------------------------------------------------------------------------------- /apps/cycle/main.js: -------------------------------------------------------------------------------- 1 | import Cycle from '@cycle/core'; 2 | import Rx from 'rx'; 3 | import { h, div, a, makeDOMDriver } from '@cycle/dom'; 4 | import makeRouter5Driver from './router5/driver'; 5 | import createRouter from './create-router'; 6 | import routes from './routes'; 7 | import emails from './data/emails'; 8 | import Nav from './components/Nav'; 9 | import Main from './components/Main'; 10 | import { shouldInterceptEvent, onClick } from './router5/link-on-click'; 11 | 12 | function main(sources) { 13 | const navigationInstruction$ = Rx.Observable.fromEvent(document, 'click', 'a') 14 | .filter(shouldInterceptEvent(sources.router)) 15 | // .map(_ => console.log(_) && _) 16 | .map(onClick(sources.router)); 17 | 18 | const navSinks = Nav(sources); 19 | const nav$ = navSinks.DOM.startWith(); 20 | // const nav$ = Rx.Observable.of('Nav'); 21 | 22 | const mainSinks = Main(sources); 23 | const main$ = mainSinks.DOM; 24 | const routerInstructions$ = mainSinks.router; 25 | 26 | const vtree$ = Rx.Observable.combineLatest( 27 | nav$, 28 | main$, 29 | (nav, main) => div('.mail-client', [ 30 | h('aside', nav), 31 | h('main', main) 32 | ]) 33 | ); 34 | 35 | return { 36 | DOM: vtree$, 37 | router: Rx.Observable.merge(navigationInstruction$, routerInstructions$) 38 | }; 39 | } 40 | 41 | function makeDataDriver() { 42 | return () => ({ 43 | emails$: Rx.Observable.of(emails) 44 | }) 45 | } 46 | 47 | Cycle.run(main, { 48 | DOM: makeDOMDriver('#app'), 49 | router: makeRouter5Driver(createRouter(routes)), 50 | data: makeDataDriver() 51 | }); 52 | -------------------------------------------------------------------------------- /apps/cycle/router5/driver.js: -------------------------------------------------------------------------------- 1 | import Rx from 'rx'; 2 | import transitionPath from 'router5.transition-path'; 3 | import routerToObservable from './router-to-observable'; 4 | 5 | const sourceMethods = [ 'getState', 'buildUrl', 'buildPath', 'matchUrl', 'matchPath', 'areStatesDescendants', 'isActive' ]; 6 | const sinkMethods = [ 'cancel', 'start', 'stop', 'navigate', 'canActivate', 'canDeactivate' ]; 7 | 8 | /** 9 | * Normalise a sink request to the router driver. 10 | * @param {String|Array} req A method name or array containing a method name and arguments 11 | * @return {Array} An array containing a method name and its arguments 12 | */ 13 | const normaliseRequest = (req) => { 14 | const normReq = Array.isArray(req) || typeof req === 'string' 15 | ? [].concat(req) 16 | : []; 17 | 18 | if (sinkMethods.indexOf(normReq[0]) === -1) { 19 | throw new Error('A Router5 sink argument should be a string (method name) or' + 20 | ' an object which first element is a valid metod name, followed by its arguments.' + 21 | ' Available sink methods are: ' + sinkMethods.join(',') + '.' 22 | ); 23 | } 24 | 25 | return normReq; 26 | } 27 | 28 | /** 29 | * Make a cycle router driver from a router5 instance 30 | * @param {Router5} router A Router5 instance 31 | * @param {Boolean} autostart Whether or not to start routing if not already started 32 | * @return {Function} A cycle sink function 33 | */ 34 | const makeRouterDriver = (router, autostart = true) => { 35 | const startRouter = () => !router.started && autostart && router.start(); 36 | 37 | // Observe router transitions 38 | const transition$ = routerToObservable(router, startRouter); 39 | 40 | // Helpers 41 | const filter = type => transition$.filter(_ => _.type === type); 42 | const slice = type => filter(type).map(_ => _.type); 43 | const sliceSlate = type => filter(type).map(({ toState, fromState }) => ({ toState, fromState })); 44 | 45 | // Filter router events observables 46 | const observables = { 47 | start$: slice('start'), 48 | stop$: slice('stop'), 49 | transitionStart$: sliceSlate('transitionStart'), 50 | transitionCancel$: sliceSlate('transitionCancel'), 51 | transitionSuccess$: sliceSlate('transitionSuccess'), 52 | transitionError$: sliceSlate('transitionError') 53 | }; 54 | 55 | // Transition Route 56 | const transitionRoute$ = transition$ 57 | .map(_ => _.type === 'transitionStart' ? _.toState : null) 58 | .startWith(null); 59 | 60 | // Error 61 | const error$ = transition$ 62 | .map(_ => _.type === 'transitionError' ? _.error : null) 63 | .startWith(null); 64 | 65 | const routeState$ = observables.transitionSuccess$ 66 | .filter(({ toState }) => toState !== null) 67 | .map(({ toState, fromState }) => { 68 | const { intersection } = transitionPath(toState, fromState); 69 | return { intersection, route: toState }; 70 | }); 71 | 72 | // Create a route observable 73 | const route$ = routeState$.map(({ route }) => route) 74 | .startWith(router.getState()); 75 | 76 | // Create a route node observable 77 | const routeNode$ = node => 78 | routeState$ 79 | .filter(({ intersection }) => intersection === node) 80 | .map(({ route }) => route) 81 | .startWith(router.getState()) 82 | .filter(route => route !== null); 83 | 84 | // Source API methods ready to be consumed 85 | const sourceApi = sourceMethods.reduce( 86 | (methods, method) => ({ ...methods, [method]: (...args) => router[method].apply(router, args) }), 87 | {} 88 | ); 89 | 90 | return request$ => { 91 | request$ 92 | .map(normaliseRequest) 93 | .subscribe( 94 | ([ method, ...args ]) => router[method].apply(router, args), 95 | err => console.error(err) 96 | ); 97 | 98 | return { 99 | ...sourceApi, 100 | ...observables, 101 | route$, 102 | routeNode$, 103 | transitionRoute$, 104 | error$ 105 | }; 106 | }; 107 | }; 108 | 109 | export default makeRouterDriver; 110 | -------------------------------------------------------------------------------- /apps/cycle/router5/link-interceptor-plugin.js: -------------------------------------------------------------------------------- 1 | import { shouldInterceptEvent, onClick } from './link-on-click'; 2 | 3 | const linkInterceptorPlugin = () => (router) => { 4 | const listener = (evt) => { 5 | if (shouldInterceptEvent(router)(evt)) { 6 | onClick(router)(evt); 7 | } 8 | }; 9 | 10 | return { 11 | name: 'LINK_INTERCEPTOR', 12 | onStart: () => { 13 | document.addEventListener('click', listener, false); 14 | }, 15 | onStop: () => { 16 | document.removeEventListener('click', listener); 17 | } 18 | } 19 | }; 20 | 21 | export default linkInterceptorPlugin; 22 | -------------------------------------------------------------------------------- /apps/cycle/router5/link-on-click.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Event button. 3 | */ 4 | const which = (evt = window.event) => null === evt.which ? evt.button : evt.which; 5 | 6 | /** 7 | * Check if `href` is the same origin. 8 | */ 9 | const sameOrigin = (href) => { 10 | var origin = location.protocol + '//' + location.hostname; 11 | if (location.port) origin += ':' + location.port; 12 | return (href && (0 === href.indexOf(origin))); 13 | }; 14 | 15 | export const shouldInterceptEvent = router => evt => { 16 | if (1 !== which(evt)) return false; 17 | if (evt.metaKey || evt.ctrlKey || evt.shiftKey) return false; 18 | if (evt.defaultPrevented) return false; 19 | 20 | // ensure link 21 | let el = evt.target; 22 | while (el && 'A' !== el.nodeName) el = el.parentNode; 23 | if (!el || 'A' !== el.nodeName) return false; 24 | 25 | // Ignore if tag has 26 | // 1. "download" attribute 27 | // 2. rel="external" attribute 28 | if (el.hasAttribute('download') || el.getAttribute('rel') === 'external') return false; 29 | 30 | // check target 31 | if (el.target) return false; 32 | 33 | if (!el.href) return false; 34 | 35 | return true; 36 | } 37 | 38 | export const onClick = router => evt => { 39 | let el = evt.target; 40 | while (el && 'A' !== el.nodeName) el = el.parentNode; 41 | const routeMatch = router.matchUrl(el.href); 42 | 43 | if (routeMatch) { 44 | evt.preventDefault(); 45 | var name = routeMatch.name; 46 | var params = routeMatch.params; 47 | return ['navigate', name, params]; 48 | } else { 49 | throw new Error(`[router5 driver] Could not match clicked hyplink href ${evt.target.href} to any known route`); 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /apps/cycle/router5/router-to-observable.js: -------------------------------------------------------------------------------- 1 | const routerToObservable = (router, onCreate) => 2 | Rx.Observable.create(observer => { 3 | const pushState = (type, isError) => (toState, fromState, ...args) => { 4 | const routerEvt = { type, toState, fromState }; 5 | observer.onNext(isError ? { ...routerEvt, error: args[0] } : routerEvt); 6 | }; 7 | const push = type => () => observer.onNext({ type }); 8 | 9 | // A Router5 plugin to push any router event to the observer 10 | const cyclePlugin = () => ({ 11 | name: 'CYCLE_DRIVER', 12 | onStart: push('start'), 13 | onStop: push('stop'), 14 | onTransitionSuccess: pushState('transitionSuccess'), 15 | onTransitionError: pushState('transitionError', true), 16 | onTransitionStart: pushState('transitionStart'), 17 | onTransitionCancel: pushState('transitionCancel') 18 | }); 19 | 20 | // Register plugin and start 21 | router.usePlugin(cyclePlugin); 22 | 23 | // On observable create callback (used to start router) 24 | onCreate && onCreate(); 25 | }); 26 | 27 | export default routerToObservable; 28 | -------------------------------------------------------------------------------- /apps/cycle/routes.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | name: 'inbox', 4 | path: '/inbox' 5 | }, 6 | { 7 | name: 'inbox.message', 8 | path: '/message/:id' 9 | }, 10 | { 11 | name: 'compose', 12 | path: '/compose' 13 | }, 14 | { 15 | name: 'contacts', 16 | path: '/contacts' 17 | } 18 | ]; 19 | -------------------------------------------------------------------------------- /apps/cycle2/components/Nav.js: -------------------------------------------------------------------------------- 1 | import { h, a } from '@cycle/dom'; 2 | 3 | const LinkFactory = router => route => a({ 4 | href: router.buildUrl(route), 5 | className: router.isActive(route, {}, true) ? 'active' : '' 6 | }, route); 7 | 8 | function Nav(sources) { 9 | const routerSource = sources.router; 10 | const Link = LinkFactory(routerSource); 11 | 12 | const nav$ = routerSource 13 | .route$ 14 | .map(route => 15 | h('nav', [ 16 | Link('a'), 17 | Link('a.a'), 18 | Link('a.b'), 19 | Link('a.c'), 20 | Link('b'), 21 | Link('b.a'), 22 | Link('b.b'), 23 | Link('b.c') 24 | ]) 25 | ); 26 | 27 | return { 28 | DOM: nav$ 29 | }; 30 | }; 31 | 32 | export default Nav; 33 | -------------------------------------------------------------------------------- /apps/cycle2/components/Route.js: -------------------------------------------------------------------------------- 1 | import { div } from '@cycle/dom'; 2 | import randomColor from 'randomcolor'; 3 | 4 | const randomBgColor = () => ({ backgroundColor: randomColor() }); 5 | 6 | const Route = node => sources => { 7 | console.log(node); 8 | const route$ = sources.router.routeNode$(node); 9 | 10 | const vDom$ = route$ 11 | .map(route => div( 12 | { className: 'item row', style: randomBgColor() }, 13 | route ? route.name : '') 14 | ); 15 | 16 | return { 17 | DOM: vDom$ 18 | }; 19 | } 20 | 21 | export default Route; 22 | -------------------------------------------------------------------------------- /apps/cycle2/create-router.js: -------------------------------------------------------------------------------- 1 | import createRouter, { loggerPlugin } from 'router5'; 2 | import browserPlugin from 'router5/plugins/browser'; 3 | import linkInterceptorPlugin from '../cycle/router5/link-interceptor-plugin'; 4 | 5 | const configureRouter = (routes) => { 6 | return createRouter(routes, { 7 | defaultRoute: 'a' 8 | }) 9 | .usePlugin(loggerPlugin) 10 | .usePlugin(browserPlugin({ 11 | useHash: true 12 | })) 13 | .usePlugin(linkInterceptorPlugin()); 14 | }; 15 | 16 | export default configureRouter; 17 | -------------------------------------------------------------------------------- /apps/cycle2/main.js: -------------------------------------------------------------------------------- 1 | import Cycle from '@cycle/core'; 2 | import Rx from 'rx'; 3 | import { h, div, a, makeDOMDriver } from '@cycle/dom'; 4 | import makeRouter5Driver from '../cycle/router5/driver'; 5 | import createRouter from './create-router'; 6 | import routes from './routes'; 7 | import Nav from './components/Nav'; 8 | import randomColor from 'randomcolor'; 9 | 10 | const randomBgColor = () => ({ backgroundColor: randomColor() }); 11 | 12 | function main(sources) { 13 | const navSinks = Nav(sources); 14 | const nav$ = navSinks.DOM.startWith(); 15 | 16 | const main$ = sources.router 17 | .routeNode$('') 18 | .flatMapLatest(route => { 19 | const childNode = route.name.split('.')[0]; 20 | 21 | const routeElm = div( 22 | { className: 'item', style: randomBgColor() }, 23 | childNode 24 | ); 25 | 26 | const Route = segment => div({ className: 'column' }, [ 27 | routeElm, 28 | segment 29 | ? div({ className: 'item', style: randomBgColor() }, segment) 30 | : div({ className: 'item' }, '_') 31 | ]); 32 | 33 | return sources.router 34 | .routeNode$(childNode) 35 | .map(route => Route(route.name.split('.')[1])); 36 | }); 37 | 38 | const vtree$ = Rx.Observable.combineLatest( 39 | nav$, 40 | main$, 41 | (nav, main) => div('.box', [ 42 | h('aside', nav), 43 | h('main', main) 44 | ]) 45 | ); 46 | 47 | return { 48 | DOM: vtree$ 49 | }; 50 | } 51 | 52 | Cycle.run(main, { 53 | DOM: makeDOMDriver('#app'), 54 | router: makeRouter5Driver(createRouter(routes)) 55 | }); 56 | -------------------------------------------------------------------------------- /apps/cycle2/routes.js: -------------------------------------------------------------------------------- 1 | const a = { 2 | name: 'a', 3 | path: '/a' 4 | }; 5 | 6 | const b = { 7 | name: 'b', 8 | path: '/b' 9 | }; 10 | 11 | const c = { 12 | name: 'c', 13 | path: '/c' 14 | }; 15 | 16 | export default [ 17 | { ...a, children: [a, b, c] }, 18 | { ...b, children: [a, b, c] } 19 | ]; 20 | -------------------------------------------------------------------------------- /apps/deku-redux/actions/draft.js: -------------------------------------------------------------------------------- 1 | export function updateTitle(title) { 2 | return { 3 | type: 'UPDATE_TITLE', 4 | title 5 | }; 6 | } 7 | 8 | export function updateMessage(message) { 9 | return { 10 | type: 'UPDATE_MESSAGE', 11 | message 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /apps/deku-redux/api.js: -------------------------------------------------------------------------------- 1 | const emails = [ 2 | { 3 | "id": "1", 4 | "mailTitle": "Why router5?", 5 | "mailMessage": "I imagine a lot of developers who will first see router5 will ask themselves the question: is it yet another router? is it any good? Why oh why do people keep writing new routers all the time? It is not always easy to see the potential of something straight away, or understand the motivations behind. I therefore decided to try to tell you more about router5, why I decided to develop an entire new routing solution, and what problems it tries to solve." 6 | }, 7 | { 8 | "id": "2", 9 | "mailTitle": "Use with React", 10 | "mailMessage": "I have just started playing with it. It does make sense to use a flux-like implementation, to provide a layer between the router and view updates." 11 | }, 12 | { 13 | "id": "3", 14 | "mailTitle": "Compose a new message", 15 | "mailMessage": "Click on compose, start to fill title and message fields and then try to navigate away by clicking on app links, or by using the back button." 16 | } 17 | ]; 18 | 19 | export function getEmails() { 20 | return emails; 21 | } 22 | 23 | export function getEmail(id) { 24 | let index; 25 | 26 | if (emails) { 27 | for (index in emails) { 28 | if (emails[index].id === id) return emails[index]; 29 | } 30 | } 31 | return null; 32 | } 33 | -------------------------------------------------------------------------------- /apps/deku-redux/components/App.js: -------------------------------------------------------------------------------- 1 | import element from 'virtual-element'; 2 | import Nav from './Nav'; 3 | import Main from './Main'; 4 | 5 | const App = { 6 | render({ props }) { 7 | return element('div', {class: 'mail-client'}, [ 8 | element('aside', {}, element(Nav)), 9 | element('main', {}, element(Main)) 10 | ]); 11 | } 12 | } 13 | 14 | export default App; 15 | -------------------------------------------------------------------------------- /apps/deku-redux/components/Compose.js: -------------------------------------------------------------------------------- 1 | import element from 'virtual-element'; 2 | import { connect } from 'deku-redux'; 3 | import { createSelector } from 'reselect'; 4 | import { updateTitle, updateMessage } from '../actions/draft'; 5 | 6 | const draftSelector = createSelector( 7 | state => state.draft, 8 | state => state.router, 9 | (draft, router) => ({ 10 | title: draft.title, 11 | message: draft.message, 12 | error: hasCannotDeactivateError(router.transitionError) 13 | }) 14 | ); 15 | 16 | function hasCannotDeactivateError(error) { 17 | return error && error.code === 'CANNOT_DEACTIVATE' && error.segment === 'compose'; 18 | } 19 | 20 | const Compose = { 21 | propTypes: { 22 | router: { source: 'router' } 23 | }, 24 | 25 | intitalState(props) { 26 | return { title: '', message: '' }; 27 | }, 28 | 29 | render({ state, props }, setState) { 30 | const { title, message, error, updateTitle, updateMessage, router } = props; 31 | 32 | const updateState = prop => evt => setState(prop, evt.target.value); 33 | router.canDeactivate('compose', !title && !message); 34 | 35 | return element('div', { class: 'compose' }, [ 36 | element('h4', {}, 'Compose a new message'), 37 | element('input', { name: 'title', value: title, onChange: updateState('title') }), 38 | element('textarea', { name: 'message', value: message, onChange: updateState('message') }), 39 | error ? element('p', {}, 'Clear inputs before continuing') : null 40 | ]); 41 | } 42 | }; 43 | 44 | export default connect(draftSelector, { updateTitle, updateMessage })(Compose); 45 | -------------------------------------------------------------------------------- /apps/deku-redux/components/Inbox.js: -------------------------------------------------------------------------------- 1 | import element from 'virtual-element'; 2 | import InboxList from './InboxList'; 3 | import Message from './Message'; 4 | import { connect } from 'deku-redux'; 5 | import { routeNodeSelector } from 'redux-router5'; 6 | import { getEmails } from '../api'; 7 | 8 | const Inbox = { 9 | displayName: 'Inbox', 10 | render({ props }) { 11 | const { route } = props; 12 | 13 | return element('div', { class: 'inbox' }, [ 14 | element(InboxList, { emails: getEmails() }), 15 | route && route.name === 'inbox.message' ? element(Message, { messageId: route.params.id, key: route.params.id }) : null 16 | ]); 17 | } 18 | }; 19 | 20 | export default connect((state) => routeNodeSelector('inbox'))(Inbox); 21 | -------------------------------------------------------------------------------- /apps/deku-redux/components/InboxItem.js: -------------------------------------------------------------------------------- 1 | import element from 'virtual-element'; 2 | 3 | const InboxItem = { 4 | propTypes: { 5 | router: {source: 'router'}, 6 | }, 7 | 8 | render({ props }) { 9 | const { mailTitle, mailMessage, router, id } = props; 10 | 11 | return element('li', { onClick: () => router.navigate('inbox.message', { id }) }, [ 12 | element('h4', {}, mailTitle), 13 | element('p', {}, mailMessage) 14 | ]); 15 | } 16 | }; 17 | 18 | export default InboxItem; 19 | -------------------------------------------------------------------------------- /apps/deku-redux/components/InboxList.js: -------------------------------------------------------------------------------- 1 | import element from 'virtual-element'; 2 | import InboxItem from './InboxItem'; 3 | 4 | const InboxList = { 5 | render({ props }) { 6 | return element( 7 | 'ul', 8 | { class: 'mail-list' }, 9 | props.emails.map(mail => element(InboxItem, { ...mail, key: mail.id })) 10 | ); 11 | } 12 | }; 13 | 14 | export default InboxList; 15 | -------------------------------------------------------------------------------- /apps/deku-redux/components/Link.js: -------------------------------------------------------------------------------- 1 | import element from 'virtual-element'; 2 | 3 | const Link = { 4 | propTypes: { 5 | name: { type: 'string' }, 6 | params: { type: 'object' }, 7 | options: { type: 'object' }, 8 | navigateTo: { type: 'function' } 9 | }, 10 | 11 | defaultProps: { 12 | params: {}, 13 | options: {} 14 | }, 15 | 16 | render({ props }) { 17 | const { name, params, options, router, navigateTo } = props; 18 | 19 | const href = router.buildUrl(name); 20 | const onClick = (evt) => { 21 | evt.preventDefault(); 22 | navigateTo(name, params, options); 23 | }; 24 | const className = router.isActive(name, params) ? 'active' : ''; 25 | 26 | return element('a', { href, onClick, 'class': className }, props.children); 27 | } 28 | }; 29 | 30 | export default Link; 31 | -------------------------------------------------------------------------------- /apps/deku-redux/components/Main.js: -------------------------------------------------------------------------------- 1 | import element from 'virtual-element'; 2 | import Inbox from './Inbox'; 3 | import Compose from './Compose'; 4 | import NotFound from './NotFound'; 5 | import { connect } from 'deku-redux'; 6 | import { routeNodeSelector } from 'redux-router5'; 7 | 8 | const components = { 9 | 'inbox': Inbox, 10 | 'compose': Compose 11 | }; 12 | 13 | const Main = { 14 | render({ props }) { 15 | const { route } = props; 16 | const segment = route ? route.name.split('.')[0] : undefined; 17 | 18 | return element(components[segment] || NotFound); 19 | } 20 | }; 21 | 22 | export default connect((state) => routeNodeSelector(''))(Main); 23 | -------------------------------------------------------------------------------- /apps/deku-redux/components/Message.js: -------------------------------------------------------------------------------- 1 | import element from 'virtual-element'; 2 | import { getEmail } from '../api'; 3 | 4 | const Message = { 5 | render({ props }) { 6 | const { mailTitle, mailMessage } = getEmail(props.messageId); 7 | 8 | return element('section', { class: 'mail' }, [ 9 | element('h4', {}, mailTitle), 10 | element('p', {}, mailMessage) 11 | ]); 12 | } 13 | }; 14 | 15 | export default Message; 16 | -------------------------------------------------------------------------------- /apps/deku-redux/components/Nav.js: -------------------------------------------------------------------------------- 1 | import element from 'virtual-element'; 2 | import Link from './Link'; 3 | import { connect } from 'deku-redux'; 4 | import { actions } from 'redux-router5'; 5 | 6 | const Nav = { 7 | propTypes: { 8 | router: { source: 'router' } 9 | }, 10 | 11 | render({ props }) { 12 | const { router, navigateTo } = props; 13 | 14 | return element('nav', {}, [ 15 | element(Link, { router, navigateTo, name: 'inbox', options: { reload: true } }, 'Inbox'), 16 | element(Link, { router, navigateTo, name: 'compose' }, 'Compose'), 17 | element(Link, { router, navigateTo, name: 'contacts' }, 'Contacts') 18 | ]); 19 | } 20 | }; 21 | 22 | export default connect( 23 | state => state.router.route, 24 | { navigateTo: actions.navigateTo } 25 | )(Nav); 26 | -------------------------------------------------------------------------------- /apps/deku-redux/components/NotFound.js: -------------------------------------------------------------------------------- 1 | import element from 'virtual-element'; 2 | 3 | const NotFound = { 4 | render() { 5 | return element('div', { class: 'not-found' }, 'Purposely not found (not a bug)'); 6 | } 7 | }; 8 | 9 | export default NotFound; 10 | -------------------------------------------------------------------------------- /apps/deku-redux/main.js: -------------------------------------------------------------------------------- 1 | import { tree, render } from 'deku'; 2 | import element from 'virtual-element'; 3 | import { storePlugin } from 'deku-redux'; 4 | import { routerPlugin } from 'deku-router5'; 5 | import App from './components/App'; 6 | import createRouter from '../create-router' 7 | import configureStore from './store'; 8 | 9 | const router = createRouter(); 10 | const store = configureStore(router); 11 | 12 | const app = tree() 13 | .use(storePlugin(store)) 14 | .set('router', router) 15 | .mount(element(App)); 16 | 17 | router.start((err, state) => { 18 | render(app, document.getElementById('app')); 19 | }); 20 | -------------------------------------------------------------------------------- /apps/deku-redux/reducers/draft.js: -------------------------------------------------------------------------------- 1 | const initialState = { 2 | title: '', 3 | message: '' 4 | }; 5 | 6 | export default function draft(state = initialState, action) { 7 | switch (action.type) { 8 | case 'UPDATE_TITLE': 9 | return { 10 | ...state, 11 | title: action.title 12 | }; 13 | 14 | case 'UPDATE_MESSAGE': 15 | return { 16 | ...state, 17 | message: action.message 18 | }; 19 | 20 | default: 21 | return state; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/deku-redux/reducers/emails.js: -------------------------------------------------------------------------------- 1 | const initialState = [ 2 | { 3 | "id": "1", 4 | "mailTitle": "Why router5?", 5 | "mailMessage": "I imagine a lot of developers who will first see router5 will ask themselves the question: is it yet another router? is it any good? Why oh why do people keep writing new routers all the time? It is not always easy to see the potential of something straight away, or understand the motivations behind. I therefore decided to try to tell you more about router5, why I decided to develop an entire new routing solution, and what problems it tries to solve." 6 | }, 7 | { 8 | "id": "2", 9 | "mailTitle": "Use with React", 10 | "mailMessage": "I have just started playing with it. It does make sense to use a flux-like implementation, to provide a layer between the router and view updates." 11 | }, 12 | { 13 | "id": "3", 14 | "mailTitle": "Compose a new message", 15 | "mailMessage": "Click on compose, start to fill title and message fields and then try to navigate away by clicking on app links, or by using the back button." 16 | } 17 | ]; 18 | 19 | export default function emails(state = initialState, action) { 20 | switch (action.type) { 21 | default: 22 | return state; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /apps/deku-redux/store.js: -------------------------------------------------------------------------------- 1 | import { compose, createStore, applyMiddleware, combineReducers } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import { router5Middleware, router5Reducer } from 'redux-router5'; 4 | import emails from './reducers/emails'; 5 | import draft from './reducers/draft'; 6 | import logger from 'redux-logger'; 7 | 8 | export default function configureStore(router, initialState = {}) { 9 | const createStoreWithMiddleware = applyMiddleware(router5Middleware(router), logger())(createStore); 10 | 11 | const store = createStoreWithMiddleware(combineReducers({ 12 | router: router5Reducer, 13 | emails, 14 | draft 15 | }), initialState); 16 | 17 | window.store = store; 18 | return store; 19 | } 20 | -------------------------------------------------------------------------------- /apps/deku/api.js: -------------------------------------------------------------------------------- 1 | const emails = [ 2 | { 3 | "id": "1", 4 | "mailTitle": "Why router5?", 5 | "mailMessage": "I imagine a lot of developers who will first see router5 will ask themselves the question: is it yet another router? is it any good? Why oh why do people keep writing new routers all the time? It is not always easy to see the potential of something straight away, or understand the motivations behind. I therefore decided to try to tell you more about router5, why I decided to develop an entire new routing solution, and what problems it tries to solve." 6 | }, 7 | { 8 | "id": "2", 9 | "mailTitle": "Use with React", 10 | "mailMessage": "I have just started playing with it. It does make sense to use a flux-like implementation, to provide a layer between the router and view updates." 11 | }, 12 | { 13 | "id": "3", 14 | "mailTitle": "Compose a new message", 15 | "mailMessage": "Click on compose, start to fill title and message fields and then try to navigate away by clicking on app links, or by using the back button." 16 | } 17 | ]; 18 | 19 | export function getEmails() { 20 | return emails; 21 | } 22 | 23 | export function getEmail(id) { 24 | let index; 25 | 26 | if (emails) { 27 | for (index in emails) { 28 | if (emails[index].id === id) return emails[index]; 29 | } 30 | } 31 | return null; 32 | } 33 | -------------------------------------------------------------------------------- /apps/deku/components/App.js: -------------------------------------------------------------------------------- 1 | import element from 'virtual-element'; 2 | import Nav from './Nav'; 3 | import Main from './Main'; 4 | 5 | const App = { 6 | render({ props }) { 7 | return element('div', {class: 'mail-client'}, [ 8 | element('aside', {}, element(Nav)), 9 | element('main', {}, element(Main)) 10 | ]); 11 | } 12 | } 13 | 14 | export default App; 15 | -------------------------------------------------------------------------------- /apps/deku/components/Compose.js: -------------------------------------------------------------------------------- 1 | import element from 'virtual-element'; 2 | import { routeNode } from 'deku-router5'; 3 | 4 | const Compose = { 5 | intitalState(props) { 6 | return { title: '', message: '' }; 7 | }, 8 | 9 | render({ state }, setState) { 10 | const { title, message } = state; 11 | 12 | const updateState = prop => evt => setState(prop, evt.target.value); 13 | 14 | return element('div', { class: 'compose' }, [ 15 | element('h4', {}, 'Compose a new message'), 16 | element('input', { name: 'title', value: title, onChange: updateState('title') }), 17 | element('textarea', { name: 'message', value: message, onChange: updateState('message') }) 18 | ]); 19 | // { warning ?

Clear inputs before continuing

: null } 20 | } 21 | }; 22 | 23 | export default routeNode('compose')(Compose); 24 | -------------------------------------------------------------------------------- /apps/deku/components/Inbox.js: -------------------------------------------------------------------------------- 1 | import element from 'virtual-element'; 2 | import InboxList from './InboxList'; 3 | import Message from './Message'; 4 | import { routeNode } from 'deku-router5'; 5 | import { getEmails } from '../api'; 6 | 7 | const Inbox = { 8 | displayName: 'Inbox', 9 | render({ props }) { 10 | const { route } = props; 11 | 12 | return element('div', { class: 'inbox' }, [ 13 | element(InboxList, { emails: getEmails() }), 14 | route && route.name === 'inbox.message' ? element(Message, { messageId: route.params.id, key: route.params.id }) : null 15 | ]); 16 | } 17 | }; 18 | 19 | export default routeNode('inbox')(Inbox); 20 | -------------------------------------------------------------------------------- /apps/deku/components/InboxItem.js: -------------------------------------------------------------------------------- 1 | import element from 'virtual-element'; 2 | 3 | const InboxItem = { 4 | propTypes: { 5 | router: {source: 'router'}, 6 | }, 7 | 8 | render({ props }) { 9 | const { mailTitle, mailMessage, router, id } = props; 10 | 11 | return element('li', { onClick: () => router.navigate('inbox.message', { id }) }, [ 12 | element('h4', {}, mailTitle), 13 | element('p', {}, mailMessage) 14 | ]); 15 | } 16 | }; 17 | 18 | export default InboxItem; 19 | -------------------------------------------------------------------------------- /apps/deku/components/InboxList.js: -------------------------------------------------------------------------------- 1 | import element from 'virtual-element'; 2 | import InboxItem from './InboxItem'; 3 | 4 | const InboxList = { 5 | render({ props }) { 6 | return element( 7 | 'ul', 8 | { class: 'mail-list' }, 9 | props.emails.map(mail => element(InboxItem, { ...mail, key: mail.id })) 10 | ); 11 | } 12 | }; 13 | 14 | export default InboxList; 15 | -------------------------------------------------------------------------------- /apps/deku/components/Main.js: -------------------------------------------------------------------------------- 1 | import element from 'virtual-element'; 2 | import { routeNode } from 'deku-router5'; 3 | import Inbox from './Inbox'; 4 | import Compose from './Compose'; 5 | import NotFound from './NotFound'; 6 | 7 | const components = { 8 | 'inbox': Inbox, 9 | 'compose': Compose 10 | }; 11 | 12 | const Main = { 13 | render({ props }) { 14 | const { route } = props; 15 | const segment = route ? route.name.split('.')[0] : undefined; 16 | 17 | return element(components[segment] || NotFound); 18 | } 19 | }; 20 | 21 | export default routeNode('')(Main); 22 | -------------------------------------------------------------------------------- /apps/deku/components/Message.js: -------------------------------------------------------------------------------- 1 | import element from 'virtual-element'; 2 | import { getEmail } from '../api'; 3 | 4 | const Message = { 5 | render({ props }) { 6 | const { mailTitle, mailMessage } = getEmail(props.messageId); 7 | 8 | return element('section', { class: 'mail' }, [ 9 | element('h4', {}, mailTitle), 10 | element('p', {}, mailMessage) 11 | ]); 12 | } 13 | }; 14 | 15 | export default Message; 16 | -------------------------------------------------------------------------------- /apps/deku/components/Nav.js: -------------------------------------------------------------------------------- 1 | import element from 'virtual-element'; 2 | import { Link } from 'deku-router5'; 3 | 4 | const Nav = { 5 | render({ props }) { 6 | return element('nav', {}, [ 7 | element(Link, { routeName: 'inbox', routeOptions: { reload: true } }, 'Inbox'), 8 | element(Link, { routeName: 'compose' }, 'Compose'), 9 | element(Link, { routeName: 'contacts' }, 'Contacts') 10 | ]); 11 | } 12 | }; 13 | 14 | export default Nav; 15 | -------------------------------------------------------------------------------- /apps/deku/components/NotFound.js: -------------------------------------------------------------------------------- 1 | import element from 'virtual-element'; 2 | 3 | const NotFound = { 4 | render() { 5 | return element('div', { class: 'not-found' }, 'Purposely not found (not a bug)'); 6 | } 7 | }; 8 | 9 | export default NotFound; 10 | -------------------------------------------------------------------------------- /apps/deku/main.js: -------------------------------------------------------------------------------- 1 | import { tree, render } from 'deku'; 2 | import element from 'virtual-element'; 3 | import { routerPlugin } from 'deku-router5'; 4 | import App from './components/App'; 5 | import createRouter from '../create-router'; 6 | 7 | const router = createRouter(true); 8 | 9 | const app = tree() 10 | .use(routerPlugin(router)) 11 | .mount(element(App)); 12 | 13 | router.start(function (err, state) { 14 | render(app, document.getElementById('app')); 15 | }); 16 | -------------------------------------------------------------------------------- /apps/react-redux/actions/draft.js: -------------------------------------------------------------------------------- 1 | export function updateTitle(title) { 2 | return { 3 | type: 'UPDATE_TITLE', 4 | title 5 | }; 6 | } 7 | 8 | export function updateMessage(message) { 9 | return { 10 | type: 'UPDATE_MESSAGE', 11 | message 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /apps/react-redux/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Nav from './Nav'; 3 | import Main from './Main'; 4 | 5 | export default function App(props) { 6 | return ( 7 |
8 | 11 | 12 |
13 |
14 |
15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /apps/react-redux/components/Compose.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { createSelector } from 'reselect'; 4 | import { bindActionCreators } from 'redux'; 5 | import { updateTitle, updateMessage } from '../actions/draft'; 6 | 7 | const draftSelector = createSelector( 8 | state => state.draft, 9 | state => state.router, 10 | (draft, router) => ({ 11 | title: draft.title, 12 | message: draft.message, 13 | error: hasCannotDeactivateError(router.transitionError) 14 | }) 15 | ); 16 | 17 | function hasCannotDeactivateError(error) { 18 | return error && error.code === 'CANNOT_DEACTIVATE' && error.segment === 'compose'; 19 | } 20 | 21 | function mapDispatchToProps(dispatch) { 22 | return bindActionCreators({ updateTitle, updateMessage }, dispatch); 23 | } 24 | 25 | class Compose extends Component { 26 | constructor(props, context) { 27 | super(props, context); 28 | this.router = context.router; 29 | } 30 | 31 | render() { 32 | const { title, message, error, updateTitle, updateMessage } = this.props 33 | this.router.canDeactivate('compose', !title && !message); 34 | 35 | return ( 36 |
37 |

Compose a new message

38 | 39 | updateTitle(evt.target.value) } /> 40 |