1000 |
1001 |
1002 |
1003 |
1004 |
1005 |
1006 |
1007 |
1008 |
1009 |
1010 |
1011 |
1012 |
1013 |
1014 |
1015 |
1016 |
1017 |
1018 |
1019 |
1020 |
1021 |
1022 |
1023 |
1024 |
1025 |
1026 |
1027 |
1028 |
1029 |
1030 |
1031 |
1032 |
1033 |
1034 |
1035 |
1036 |
1037 |
1038 |
1039 |
1040 |
1041 |
1042 |
1043 |
1044 |
1045 |
1046 |
1047 |
1048 |
1049 |
1050 |
1051 |
1052 |
1053 |
1054 |
1055 |
1056 |
1057 |
1058 |
1059 |
1060 |
1061 |
1062 |
1063 |
1064 |
1065 |
1066 |
1067 |
1068 |
1069 |
1070 |
1071 |
1072 |
1073 |
1074 |
1075 |
1076 |
1077 |
1078 |
1079 |
1080 |
1081 |
1082 |
1083 |
1084 |
1085 |
1086 |
1087 |
1088 |
1089 |
1090 |
1091 |
1092 |
1093 |
1094 |
1095 |
1096 |
1097 |
1098 |
1099 |
1100 |
1101 |
1102 |
1103 |
1104 |
1105 |
1106 |
1107 |
1108 |
1109 |
1110 |
1111 |
1112 |
1113 |
1114 |
1115 |
1116 |
1117 |
1118 |
1119 |
1120 |
1121 |
1122 |
1123 |
1124 |
1125 |
1126 |
1127 |
1128 |
1129 |
1130 |
1131 |
1132 |
1133 |
1134 |
1135 |
1136 |
1137 |
1138 |
1139 |
1140 |
1141 |
1142 |
1143 |
1144 |
1145 |
1146 |
1147 |
1148 |
1149 |
1150 |
1151 |
1152 |
1153 |
1154 |
1155 |
1156 |
1157 |
1158 |
1159 |
1160 |
1161 |
1162 |
1163 |
1164 |
1165 |
1166 |
1167 |
1168 |
1169 |
1170 |
1171 |
1172 |
1173 |
1174 |
1175 |
1176 |
1177 |
1178 |
1179 |
1180 |
1181 |
1182 |
1183 |
1184 |
1185 |
1186 |
1187 |
1188 |
1189 |
1190 |
1191 |
1192 |
1193 |
1194 |
1195 |
1196 |
1197 |
1198 |
1199 |
1200 |
1201 |
1202 |
1203 |
1204 |
1205 |
1206 |
1207 |
1208 |
1209 |
1210 |
1211 |
1212 |
1213 |
1214 |
1215 |
1216 |
1217 |
1218 |
1219 |
1220 |
1221 |
1222 |
1223 |
1224 |
1225 |
1226 |
1227 |
1228 |
1229 |
1230 |
1231 |
1232 |
1233 |
1234 |
1235 |
1236 |
1237 |
1238 |
1239 |
1240 |
1241 |
1242 |
1243 |
1244 |
1245 |
1246 |
1247 |
1248 |
1249 |
1250 |
1251 |
1252 |
1253 |
1254 |
1255 |
1256 |
1257 |
1258 |
1259 |
1260 |
1261 |
1262 |
1263 |
1264 |
1265 |
1266 |
1267 |
1268 |
1269 |
1270 |
1271 |
1272 |
1273 |
1274 |
1275 |
1276 |
1277 |
1278 |
1279 |
1280 |
1281 |
1282 |
1283 |
1284 |
1285 |
1286 |
1287 |
1288 |
1289 |
1290 |
1291 |
1292 |
1293 |
1294 |
1295 |
1296 |
1297 |
1298 |
1299 |
1300 |
1301 |
1302 |
1303 |
1304 |
1305 |
1306 |
1307 |
1308 |
1309 |
1310 |
1311 |
1312 |
1313 |
1314 |
1315 |
1316 |
1317 |
1318 |
1319 |
1320 |
1321 |
1322 |
1323 |
1324 |
1325 |
1326 |
1327 |
1328 |
1329 |
1330 |
1331 |
1332 |
1333 |
1334 |
1335 |
1336 |
1337 |
1338 |
1339 |
1340 |
1341 |
1342 |
1343 |
1344 |
1345 |
1346 |
1347 |
1348 |
1349 |
1350 |
1351 |
1352 |
1353 |
1354 |
1355 |
1356 |
1357 |
1358 |
1359 |
1360 |
1361 |
1362 |
1363 |
1364 |
1365 |
1366 |
1367 |
1368 |
1369 |
1370 |
1371 |
1372 |
1373 |
1374 |
1375 |
1376 |
1377 |
1378 |
1379 |
1380 |
1381 |
1382 |
1383 |
1384 |
1385 |
1386 |
1387 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## Change log
2 |
3 | #### 0.5.2 - 2015/09/14
4 | - Fixed bug - scroll position on back
5 | - route component now carrys props to component
6 |
7 | #### 0.5.1 - 2015/09/12
8 | - Router now keeps scroll position on routes
9 | - New router and route react component (no nasting yet) chooses first route
10 | - navigateTo action now has option parameter
11 | - new option 'scroll' will scroll to least position on the route
12 | - Link react component also updated to support option prop
13 |
14 |
15 | #### 0.4.6 - 2015/09/07
16 | - Pushstate will fallback to refresh if not available
17 | - initUniversal now also accepts initial state
18 |
19 |
20 | #### 0.4.4 - 2015/09/06
21 | - redux 2.0
22 | - scroll up on ROUTE_NAVIGATION, preserve scroll on back/forward (standard behaviour)
23 | - renamed export rotuerActions to tinyActions
24 | - renamed export middleware to tinyMiddleware
25 | - and minor clean ups and documentation fix
26 |
27 | #### 0.4.2 - 2015/09/04
28 | - router.previous now holds the previous url
29 | - navigateTo now has optional boolean parameter for silent navigation
30 | - fix utils.check
31 |
32 | #### 0.4.0 - 2015/09/04
33 | - removed paths and subpath from router
34 | - Introduced route definitions
35 | - utils have new functions (set,setRoutes,match,check)
36 | - Removed RTR_ prefixing from action creators and actions
37 |
38 | #### 0.3.0 - 2015/09/01
39 | - New store enhancer
40 | - Router obj has more properties, (paths, subpath)
41 | - new server side resolution
42 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Redux Tiny Router
2 |
3 | A Router made for Redux and made for universal apps! stop using the router as a controller... it's just state!
4 |
5 | It's simple, and it's small, example app in [react-redux-tiny](https://github.com/Agamennon/react-redux-tiny)
6 |
7 | warning! changing fast, for the new stuff look at the [changelog](https://github.com/Agamennon/redux-tiny-router/blob/master/CHANGELOG.md) , readme a little behind
8 |
9 | ### Using client side OPTION1 (store enhancer) OPTION2 (bring all yourself) at the end.
10 |
11 |
12 | Create your store with the redux-tiny-router applyMiddleware
13 |
14 | ```javascript
15 | import {createStore} from 'redux';
16 | import {applyMiddleware} from 'redux-tiny-router'
17 | import * as yourReducers from './someplace'
18 | import yourMiddleware from './someotherplace'
19 |
20 | // Don't combine the reducers the middleware does this for you
21 | let middleware = [yourMiddleware]; //and others you use
22 | var finalCreateStore = applyMiddleware(...middleware)(createStore);
23 | store = finalCreateStore(yourReducers,{}); //just pass your reducers object
24 | ```
25 |
26 | and you are DONE!
27 |
28 | If you enter any paths to your url you will notice that you also have a router object on your state containing relevant information about the current route,
29 | but how to change the route inside the app?
30 |
31 | ### redux-tiny-router action creators
32 | Yup, you call an action, first import the router actions:
33 |
34 | ```javascript
35 | import {tinyActions} from 'redux-tiny-router'
36 | //navigates to a route with optional search object
37 | dispatch(tinyActions.navigateTo('/somepath', {message:1}));
38 | ```
39 |
40 | The router will navigate to (/somepath?message=1) and set the router object with the info, for that it fires an action `type:'ROUTER_NAVIGATION'`
41 | that you can use to intercept the action on your middleware and do all sorts of interesting things, more later...
42 |
43 | Some more cool actions:
44 |
45 | ```javascript
46 | preventNavigation(); //bonus! call this with a string and will prevent the user from navigating away from the page too!
47 | ```
48 |
49 | Does what it says (it also blocks forward and back), after you call this you lock navigation, useful if the user is on a form and you want to warn him about pending changes,
50 | if the user attempts to navigate, it fires and action `type:PREVENTED_NAVIGATION_ATTEMPTED` that will set a field in your router with
51 | the attempted route, you actually don't need to worry about this, you can just check on your app if the value on the router.attemptedOnPrevent
52 | contains a value (this value is the attempted url) in this case you can show a pop-up warning the user of pending changes.
53 | But what if the users whats to navigate away?
54 |
55 | ```javascript
56 | doPreventedNavigation();
57 | ```
58 | You call this action!, it will read the value from router.attemptedOnPrevent and navigate there! (it handles back and forward buttons just fine)
59 |
60 | ```javascript
61 | allowNavigation();
62 | ```
63 | That just allows navigation again. if you want to handle the navigate after confirm implementation yourself.
64 |
65 |
66 | ### Basic routing
67 |
68 | You could just do this, inside your react app,
69 |
70 | ```javascript
71 | @connect((state ) => {
72 | return {
73 | router:state.router
74 | }
75 | })
76 | const Comp = React.createClass({
77 | render() {
78 | switch (this.props.router.path) {
79 | case '/':
80 | return ;
81 | case '/other':
82 | return
83 | default:
84 | return ;
85 | }
86 | }
87 | });
88 |
89 | ```
90 |
91 | The basic idea is this, no more controller components, it's just state, the reducer in redux-tiny-router, will feed your state
92 | with a router object that represent all the nuances of the url automatically
93 |
94 | ### What do i get in this router object?
95 |
96 | for an url like `some/cool/path?name=gui`
97 |
98 | ```javascript
99 |
100 | "router": {
101 | "url": "/some/cool/path?name=gui",
102 | "src": null,
103 | "splat": null,
104 | "params": {},
105 | "path": "/some/cool/path",
106 | "paths": [
107 | "/",
108 | "some",
109 | "cool",
110 | "path"
111 | ],
112 | "query": {
113 | "name": "gui"
114 | }
115 | }
116 |
117 | ```
118 |
119 | The `url` property hold the exact url you see in the browser, `src`, `splat`, and `params` will only have a value if
120 | you specify some route configuration (more on this later) if your routes are not too complicated you will have no need
121 | for those, `path` it's the url minus the query string `?name=gui` in this example, `paths` is an array with all individual elements
122 | and `query` holds the query string turned into a object.
123 |
124 | You are free to use any of those to decide what component you will render, so this brings the "controlling"
125 | back to your app.
126 |
127 |
128 | ### Configuring routes, in case you need it
129 |
130 | redux-tiny-router internally uses a slightly modified version of a tiny route matching library called [http-hash](https://github.com/Matt-Esch/http-hash)
131 | with that you can choose to define some routes, those definitions will populate the router object `src` `splat` and `params` properties,
132 | lets take a look:
133 |
134 | First bring into your project the router utils, naturally configure your routes before you need'em
135 |
136 | ```javascript
137 |
138 | import {utils} from 'redux-tiny-router';
139 |
140 | //this will configure a route (the second part of the path will be a parameter called test)
141 | //This matches /foo/ but "/" it will match /foo/somestuf but will not match /foo/somestuff/morestuff
142 | utils.set('/foo/:test/')
143 |
144 | ```
145 |
146 | If you navigate to `/foo/cool` the router now knows, since you configured a matching route, how to populate `src`,
147 | in this case it will be set to `/foo/:test/` src hold what pattern was matched with the url, this is quite useful
148 | for your react app to decide what to render (examples later...) `params` will have the object containing the route params,
149 | in this case `{test:cool}`, splat will be `null`. **The router does not care if the url matches the route, if it does not,
150 | you just don't get values for `src` `params` and `splat`. Think about route definitions as teaching the router how to extract
151 | extra information that you need.**
152 |
153 | What is a splat? well it's the wild-card *, lest add another route definition.
154 |
155 | ```javascript
156 |
157 | //this will map to /test/ but "/">/ ...
158 | utils.set('/foo/:test/*')
159 |
160 |
161 | ```
162 |
163 | Let's trow `/foo/some/long/stuff` as a url, now `src` will be `/foo/:test/*` params `{test:some}`
164 | and splat `/long/stuff` (splat is anything that came after the `*`)
165 |
166 |
167 | For convenience you can use `utils.setRoutes` pass an array of definitions to set them all with one call:
168 |
169 | ```javascript
170 |
171 | utils.setRoutes([
172 | '/foo/:test/',
173 | '/foo/:test/*'
174 | ...
175 | ...
176 | ...
177 | ]);
178 |
179 | ```
180 |
181 | A more specific definition have precedence over a broad definition so `/foo/something` in the above definitions
182 | could match both route definitions, but `src` will be set to `/foo/:test/` as it's more specific. (the order of route definitions does not matter).
183 |
184 |
185 | I told you that `src` is useful, well any pace of state from router can be useful but `src` is specially cool
186 | lets look of how to use this in a react app (nesting routes):
187 |
188 | Consider the url `/foo/some/more`
189 |
190 | ```javascript
191 |
192 | //before...
193 | utils.setRoutes([
194 | '/',
195 | '/foo/*',
196 | '/foo/some'
197 | ]);
198 |
199 | const Comp = React.createClass({
200 | render() {
201 | switch (this.props.router.src) { //looking at src property
202 | case '/':
203 | return ;
204 | case '/foo/*':
205 | return
206 | case '/foo/some': //this have to be here as a more specific route like /foo/some would be matched here (src would = '/foo/some')
207 | return
208 | default:
209 | return ;
210 | }
211 | }
212 | });
213 |
214 | ```
215 |
216 | Foo could be:
217 |
218 | ```javascript
219 |
220 | const Foo = React.createClass({
221 | render() {
222 | switch (this.props.router.splat) { //notice SPLAT
223 | case '/some':
224 | return
225 | case '/some/more'
226 | return
227 | default:
228 | return ;
229 | }
230 | }
231 | });
232 |
233 | ```
234 |
235 | That would render `` Just remember that this example is somewhat arbitrary, in this case you don't even "need" to define routes, you could have used
236 | `router.paths[1]` on Comp and `router.paths[2]` on Foo, like so:
237 |
238 |
239 | ```javascript
240 |
241 | const Comp = React.createClass({
242 | render() {
243 | var paths = this.props.router.paths;
244 | if (paths[0] === '/') return
245 | if (paths[1] === '/foo') return
246 | return
247 | }
248 | });
249 |
250 |
251 | ```
252 |
253 | on Foo
254 |
255 | ```javascript
256 |
257 | const Foo = React.createClass({
258 | render() {
259 | var paths = this.props.router.paths;
260 | if (paths[3] === 'some') return
261 | if (paths[4] === 'more') return
262 | return
263 | }
264 | });
265 |
266 |
267 | ```
268 |
269 | Remember route definitions only add more details, you can use any peace of state you need and any javascript knowledge you have to render your app,
270 | but just to give you yet more power, to guarantee you can do anything i could think of, have a look at this puppy `utils.match(definition,url)`, this util will return a full router obj using a on the fly route definition, if the url match the definition
271 | you also get, `src` `splat` and `params`, so you could without adding previous route definitions, make a one time check on `src` for even more flexibility.
272 | Think about it, in the first example i had to add another case for the more specific route (because i added it) is an artificial problem but will help to illustrate.
273 |
274 | ```javascript
275 |
276 | const Comp = React.createClass({
277 | render() {
278 | const url = this.props.router.url;
279 | const match = utils.match;
280 | if (match('/',url).src) return //.src have a value with the url matches the definition
281 | if (match('/foo/*',url).src) return
282 | return
283 | }
284 | });
285 |
286 |
287 | ```
288 |
289 | on Foo, we are not going to use `utils.match` instead we will use `utils.check`, match returns an
290 | object with all those state things, you can use utils.check, it returns a boolean, if the only thing
291 | you need is to check if the url matches a definition (that is our case on both components!)
292 |
293 |
294 | ```javascript
295 |
296 | const Foo = React.createClass({
297 | render() {
298 | const url = this.props.router.url;
299 | const check = utils.check;
300 | if (check('/some',url)) return
301 | if (check('foo/some/more/stuff/*',url)) return
302 | if (check('/some/more',url)) return
303 | return
304 | }
305 | });
306 |
307 |
308 | ```
309 |
310 | ### Understanding how react-tiny-router works
311 |
312 | When the user enters a url on the browser, presses the back or forward buttons, or the navigateTo action creator is called,
313 | redux-tiny-router will dispatch an action:
314 |
315 |
316 | ```javascript
317 | {
318 | type:ROUTER_NAVIGATION,
319 | router:router
320 | ...
321 | }
322 | ```
323 |
324 | The router property already contains a populated router object, when this action reaches the router middleware, at the end of the middleware chain,
325 | it will read the action.router.url property and set the browser with that url, it will then reach the router reducer, that will make router part of the state.
326 | It's quite simple really, but now that you know this, it's easy to create a middleware to intercept this action.
327 |
328 | let's make something cool here, if the user is going to a secure place in your app let's redirect him to /login
329 | you can see the full implementation in [react-redux-tiny](https://github.com/Agamennon/react-redux-tiny) example app.
330 |
331 | inside your middleware..
332 |
333 | ```javascript
334 |
335 | if (action.type === 'ROUTER_NAVIGATION'){
336 | const {url,path} = action.router;
337 | const isSecurePlace = utils.check('/secure/*',url);
338 | const loggedIn = getState().data.user; //presume that the data part of your state will hold the user
339 | if (path === '/login') //if user wants to login thats ok!
340 | return next(action);
341 | if (isSecurePlace && !loggedIn){
342 | dispatch(tinyActions.preventedNavigationAttempted(url)); //router will now store the attempted url (you can use this to send him where he wanted to go after auth)
343 | dispatch(tinyActions.navigateTo('/login')); //navigate to /login
344 | return; // this will stop further ROUTER_NAVIGATION processing, the action it will never reach the router middleware or the reducer
345 | }
346 | return next(action);
347 | }
348 |
349 | //the rest of your middleware
350 | ....
351 |
352 | ```
353 |
354 | In there, is business as usual, you could naturally dispatch your own actions with part of the router state,
355 | to your own part of the state and point your app there if you want, dispatch actions based on some part of the
356 | router state to fetch some data, or whatever you need!. You can even do redirects differently, by calling `utils.urlToRouter(url)` you get
357 | new router object based on the url you fed it, now place that on action.router (to replace it) and send it forward `next(action)`
358 | and you are done. You could of course just dispatch a navigateTo action and not return next(action) as we did on the example above,
359 | just showing how you can monkey around in your reducer, as this router works in a redux flow and it's just state, you
360 | have plenty of opportunity to interact.
361 |
362 |
363 | You could do your all your routing just by looking at the router or your "own" state, fetch data in the middleware or in your component,
364 | the router does not care...
365 |
366 |
367 | ### The utils
368 | the same utils the router uses you can use it too
369 | import {utils} from 'react-tiny-router';
370 | the ones are:
371 |
372 | Returns a router object:
373 |
374 | ```javascript
375 | utils.urlToRouter('/some/cool/url?param=10¶m2=nice')
376 | ```
377 |
378 | Takes a path and a search object, returns a query string:
379 |
380 | ```javascript
381 | utils.toQueryString('/some/cool/url',{param:10,param2:'nice') //it will spill the url used above
382 | ```
383 |
384 | Set a route definition
385 |
386 | ```javascript
387 | utils.set('/*')
388 | ```
389 |
390 | Sets a bunch of route definitions
391 |
392 | ```javascript
393 | utils.setRoutes([
394 | '/*',
395 | '/foo',
396 | ]),
397 |
398 | ```
399 |
400 | Returns a router object, also sets this router object with route definitions if it matches
401 |
402 | ```javascript
403 | utils.match('/foo',url);
404 | ```
405 |
406 | Returns true if the url matches the definition false otherwise
407 |
408 | ```javascript
409 | utils.check('/foo',url);
410 | ```
411 |
412 |
413 | ### Universal Apps
414 |
415 | redux-tiny-router has a initUniversal function, that returns a promise, this promise resolves with data.html (with the rendered app)
416 | and data.state with your state, now just send those in, and presto, redux-tiny-router handles async on react just fine as long as all
417 | async operations are done using actions, and that those actions ether return a promise or have an attribute that is a promise, you can
418 | even load data on componentWillMount on react applications, you also don't need to wait or synchronize any async operations, as the
419 | router will wait and re-render server side if on the first render, async actions where fired modifying the state.
420 | This makes the client not only receive the complete state of your app but also the final render from that state.
421 |
422 | This example use a ejs template as it's quite elegant for this, or you could just use a react component
423 |
424 | ```javascript
425 | import createStore from '../your/path/create-store.js'; //(this should return a function that creates a store)
426 | import {reduxTinyRouter} from 'redux-tiny-router';
427 | import Component from '../shared/components/Layout.jsx';
428 |
429 |
430 | reduxTinyRouter.initUniversal(url,createStore,Component).then((data)=>{
431 | res.render('index', {
432 | html: data.html,
433 | payload: JSON.stringify(data.state),
434 | });
435 | });
436 | ```
437 |
438 | The ejs template:
439 |
440 | ```html
441 |
442 |
443 |
444 | Redux Tiny Universal Example
445 |
446 |
447 |
<%- html %>
448 |
449 |
450 |
451 |
452 |
453 |
454 | ```
455 |
456 | And on the client:
457 |
458 | ```javascript
459 |
460 | import React from 'react';
461 | import Layout from '../shared/components/Layout.jsx'; //your react app
462 | import createStore from '../shared/redux/create-store.js';
463 |
464 | const store = createStore(window.__DATA__,window.location.href);
465 |
466 | document.addEventListener('DOMContentLoaded', () => {
467 | React.render(,
468 | document.getElementById('app')
469 | );
470 | });
471 |
472 | ```
473 | And it works, the example universal app [react-redux-tiny](https://github.com/Agamennon/react-redux-tiny) can show you more!
474 |
475 |
476 | ### Using client side OPTION2 (bring stuff by hand) if OPTION1 is robbing you of applyMiddleware form a third party or you combine your reducers in a fancy way
477 |
478 | Create your store with the redux-tiny-router middleware and reducer
479 |
480 | ```javascript
481 | import { createStore, applyMiddleware, combineReducers} from 'redux';
482 | import {tinyMiddleware ,tinyReducer} from 'redux-tiny-router';
483 | import * as yourReducers from './reducers'
484 |
485 | let middleware = [appMiddleware,tinyMiddleware]; //notice tinyMiddleware must be the last one;
486 | //middleware.unshift(tinyUniversal); //import tinyUniversal and uncomment if you are building a universal app
487 | var reducer = combineReducers(Object.assign({},tinyReducer,yourReducers));
488 | var finalCreateStore = applyMiddleware(...middleware)(createStore);
489 | store = finalCreateStore(reducer,{});
490 | ```
491 |
492 | if you are building an universal app, you need to bring
493 | Standard stuff, for now you just added a middleware and a reducer from redux-tiny-router, you should turn this in to a function
494 | that returns the store that you can import for convenience and if you plan on doing an Universal app
495 |
496 | Now you only have to call the init function with the store before you render your app:
497 |
498 | ```javascript
499 | import { reduxTinyRouter } from 'redux-tiny-router';
500 |
501 | reduxTinyRouter.init(store);
502 |
503 | React.render(,
504 | document.getElementById('app')
505 | );
506 |
507 | ...
508 | ```
509 | DONE!
510 |
511 | Inspired by cerebral reactive router
512 |
513 | ### License
514 |
515 | MIT
516 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redux-tiny-router",
3 | "version": "0.5.3",
4 | "author": {
5 | "name": "Guilherme Guerchmann",
6 | "email": "gui@guerch.net",
7 | "url": "http://www.guerch.net"
8 | },
9 | "description": "A Router made for Redux and made for universal apps! stop using the router as a controller... its just state!",
10 | "main": "bin/reduxTinyRouter.js",
11 | "keywords": [
12 | "redux",
13 | "router",
14 | "universal",
15 | "tiny"
16 | ],
17 | "engines": {
18 | "node": ">= 0.12.0"
19 | },
20 | "repository": {
21 | "type": "git",
22 | "url": "https://github.com/Agamennon/redux-tiny-router.git"
23 | },
24 | "license": "MIT",
25 | "dependencies": {
26 | "http-hash": "^2.0.0",
27 | "query-string": "^2.4.0",
28 | "react": "^0.13.3",
29 | "redux": "^2.0.0"
30 | },
31 | "devDependencies": {
32 | "babel-loader": "^5.3.2",
33 | "babel-runtime": "^5.8.20",
34 | "path": "^0.11.14"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/actions/actions.js:
--------------------------------------------------------------------------------
1 |
2 | import {utils} from '../utils/utils.js';
3 |
4 |
5 | // ****************************** NAVIGATION *****************************************
6 | export function navigateTo(path, params, option){
7 |
8 | if (typeof params === 'string'){
9 | option = params;
10 | params = undefined;
11 | }
12 | var url = utils.toQueryString(path,params);
13 | return {
14 | type:'RTR_ACTION',
15 | work:(dispatch,getState)=>{
16 | dispatch(urlChanged(url,option))
17 | }
18 |
19 | };
20 | }
21 |
22 |
23 |
24 | export function urlChanged(url, option){
25 |
26 | return {
27 | type:'RTR_ACTION',
28 | work:(dispatch,getState)=>{
29 | var prevent = getState().router.preventNavigation;
30 | if (prevent === true){
31 | dispatch(preventedNavigationAttempted(url));
32 | } else {
33 | dispatch(changeUrl(url,option));
34 | }
35 | }
36 |
37 | };
38 | }
39 |
40 |
41 |
42 |
43 | export function changeUrl(url, option){
44 | var router = utils.urlToRouter(url);
45 |
46 | return {
47 | type:'ROUTER_NAVIGATION',
48 | router,
49 | option
50 | };
51 | }
52 |
53 |
54 |
55 |
56 | // ************************* NAVIGATION PREVENTION *************************************
57 | export function preventNavigation(message){
58 | return {
59 | type:'PREVENT_NAVIGATION',
60 | message
61 | }
62 | }
63 |
64 | export function allowNavigation(){
65 | return {
66 | type:'ALLOW_NAVIGATION'
67 | }
68 | }
69 |
70 | export function preventedNavigationAttempted(url){
71 |
72 | return {
73 | type:'PREVENTED_NAVIGATION_ATTEMPTED',
74 | url
75 | }
76 | }
77 |
78 | export function doPreventedNavigation(){
79 |
80 | return {
81 | type:'RTR_ACTION',
82 | work:(dispatch,getState)=>{
83 | var url = getState().router.attemptedOnPrevent;
84 | if (url){
85 | dispatch(allowNavigation());
86 |
87 | if (url === '_back'){
88 | history.back();
89 | return
90 | }
91 | if (url === '_forward'){
92 | history.forward();
93 | return
94 | }
95 |
96 | dispatch(changeUrl(url));
97 |
98 | } else {
99 | console.warn('user have not attempted navegating under prevent!');
100 | }
101 |
102 | }
103 |
104 | };
105 | }
106 |
107 |
108 |
109 | // ************************* UNIVERSAL HELPERS *************************************
110 | export function universalSetPeniding(val, done){
111 | return {
112 | type:'UNIVERSAL_SET_PENDING',
113 | val,
114 | done
115 | }
116 | }
117 |
118 | export function universalPromiseDone(){
119 | return {
120 | type:'UNIVERSAL_PROMISE_DONE'
121 | }
122 | }
123 |
124 |
125 | export function syncActionsDone(){
126 |
127 | return {
128 | type:'UNIVERSAL_SYNC_ACTIONS_DONE'
129 | }
130 | }
131 |
132 | export function syncActionsPending(){
133 | return {
134 | type:'UNIVERSAL_SYNC_ACTIONS_PENDING'
135 | }
136 | }
137 |
138 |
--------------------------------------------------------------------------------
/src/components/link.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import qs from 'query-string';
3 | import * as actions from '../actions/actions.js'
4 |
5 | export class Link extends React.Component {
6 |
7 | static contextTypes = { store: React.PropTypes.any };
8 |
9 | click(e){
10 | e.preventDefault();
11 | this.context.store.dispatch(actions.navigateTo(this.props.path,this.props.search,this.props.option));
12 | }
13 |
14 | render() {
15 | const { path, search, option, ...rest } = this.props;
16 | let href = `${path}`;
17 | if (search) href = href + `?${qs.stringify(search)}`;
18 | return (
19 | {this.props.children}
20 | );
21 | }
22 | }
23 |
24 |
--------------------------------------------------------------------------------
/src/components/route.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import qs from 'query-string';
3 | import * as actions from '../actions/actions.js';
4 | import {utils} from '../utils/utils.js';
5 |
6 | class Null extends React.Component {
7 | render() {
8 | return null
9 | }
10 | }
11 |
12 | export class Route extends React.Component {
13 | static contextTypes = { store: React.PropTypes.any
14 | };
15 |
16 | render() {
17 | let { path, url, component, ...rest } = this.props;
18 | url = url ? url : this.context.store.getState().router.url;
19 | var Response = (utils.check(this.props.path,url)) ? component: Null;
20 | return
21 | }
22 | }
23 |
24 |
--------------------------------------------------------------------------------
/src/components/router.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import qs from 'query-string';
3 | import * as actions from '../actions/actions.js'
4 | import {utils} from '../utils/utils.js';
5 |
6 | class Null extends React.Component {
7 | render() {
8 | return null
9 | }
10 | }
11 | export class Router extends React.Component {
12 | static contextTypes = { store: React.PropTypes.any };
13 | render() {
14 | var routerUrl = this.context.store.getState().router.url;
15 | var validComponents = [];
16 | let {NotFound, ...rest } = this.props;
17 |
18 |
19 | function MapAndPush(children){
20 | var done = false;
21 | React.Children.forEach(children,(element,index)=>{
22 | if (done) return;
23 | let { path, url, component, ...rest } = element.props;
24 | url = url || routerUrl;
25 | if (utils.check(path,url)) {
26 | done = true;
27 | validComponents.push({
28 | component,
29 | rest
30 | });
31 |
32 | }
33 | }
34 | )}
35 | MapAndPush(this.props.children);
36 | var Root = NotFound;
37 | if (validComponents.length > 0) {
38 | Root = validComponents[0].component;
39 | Root.props = validComponents[0].rest;
40 |
41 | }
42 | return
43 |
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/enhancer/enhancer.js:
--------------------------------------------------------------------------------
1 | import {init,initUniversal} from '../redux-tiny-router/redux-tiny-router.js';
2 | import {middleware as tinyMiddleware} from '../middleware/middleware.js';
3 | import {universal as tinyUniversal} from '../middleware/universal.js';
4 | import {router as tinyReducer} from '../reducer/reducer.js';
5 | import {combineReducers} from 'redux';
6 |
7 | function compose(...funcs) {
8 | return funcs.reduceRight((composed, f) => f(composed));
9 | }
10 |
11 | function is_server() {
12 | return ! (typeof window != 'undefined' && window.document);
13 | }
14 |
15 | export function applyMiddleware(...middlewares) {
16 | return (next) => (reducer, initialState) => {
17 |
18 | if (is_server()){
19 | global.__CLIENT__ = false;
20 | global.__UNIVERSAL__ = global.__UNIVERSAL__ || false;
21 | } else {
22 | window.__CLIENT__ = true;
23 | window.__UNIVERSAL__ = window.__UNIVERSAL__ || false;
24 | }
25 |
26 |
27 | function reducerEnhancer (state,action){
28 | Object.assign(reducer,tinyReducer,reducer);
29 | var res = combineReducers(reducer);
30 | return res(state,action);
31 |
32 | }
33 |
34 | var store = next(reducerEnhancer, initialState);
35 |
36 |
37 | middlewares.push(tinyMiddleware);
38 | if (__UNIVERSAL__ && !__CLIENT__){
39 | middlewares.unshift(tinyUniversal);
40 | }
41 |
42 | var dispatch = store.dispatch;
43 | var chain = [];
44 |
45 | var middlewareAPI = {
46 | getState: store.getState,
47 | dispatch: (action) => dispatch(action)
48 | };
49 | chain = middlewares.map(middleware => middleware(middlewareAPI));
50 | dispatch = compose(...chain, store.dispatch);
51 |
52 | var result = {
53 | ...store,
54 | dispatch
55 | };
56 |
57 | if (__CLIENT__){
58 | init(result);
59 | }
60 |
61 | return result;
62 |
63 |
64 | };
65 | }
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 |
2 | import * as tinyActions from './actions/actions.js'
3 | export {tinyActions}
4 | export {middleware as tinyMiddleware} from './middleware/middleware.js'
5 | export {universal as tinyUniversal} from './middleware/universal.js';
6 | import * as reduxTinyRouter from './redux-tiny-router/redux-tiny-router.js';
7 | export {reduxTinyRouter}
8 | export {applyMiddleware} from './enhancer/enhancer'
9 | export {router as tinyReducer} from './reducer/reducer.js';
10 | export {utils} from './utils/utils';
11 | export {Link} from './components/link.jsx';
12 | export {Route} from './components/route.jsx';
13 | export {Router} from './components/router.jsx';
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/middleware/middleware.js:
--------------------------------------------------------------------------------
1 | import {utils} from '../utils/utils.js';
2 | import * as actions from '../actions/actions.js';
3 |
4 | function changeBrowserURL(action){
5 |
6 | const option = action.option;
7 |
8 | function setScroll(pos){
9 | setTimeout(()=>{
10 | document.body.scrollTop = document.documentElement.scrollTop = pos;
11 | },0);
12 | }
13 | if (option === 'scroll'){
14 | const path = action.router.path || '/';
15 | const pos = utils.scrollpos[path] || 0;
16 | setScroll(pos);
17 | } else if(option !== 'popEvent') {
18 | setScroll(0);
19 | }
20 | switch (option) {
21 | case ('silent'):
22 | history.replaceState(utils.navindex, null, action.router.previous);
23 | return;
24 | case ('popEvent'): //pop event already poped the url
25 | return;
26 | default:
27 | utils.navindex++;
28 | history.pushState(utils.navindex, null, action.router.url);
29 | }
30 |
31 | }
32 |
33 | //todo pass in option to track a particular element
34 | function storeScroll (path){
35 | path = path || '/';
36 | utils.scrollpos[path] = document.body.scrollTop;
37 |
38 | }
39 |
40 | export function middleware ({ dispatch, getState }) {
41 | return (next) => {
42 | return (action) => {
43 | //the main action concerning the user
44 | if (action.type === 'ROUTER_NAVIGATION'){
45 | if (__CLIENT__) {
46 |
47 | storeScroll(getState().router.path);
48 | if (history.pushState) { //fallback to Refresh
49 | changeBrowserURL(action);
50 | } else if (action.option !== 'popEvent') {
51 | window.location.assign(action.router.url);
52 | return
53 | }
54 | }
55 | return next(action)
56 | }
57 |
58 | //special thunk just for the router
59 | if (action.type === 'RTR_ACTION'){
60 | return action.work(dispatch,getState);
61 | }
62 | return next(action);
63 | }
64 | }
65 |
66 | }
67 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/src/middleware/universal.js:
--------------------------------------------------------------------------------
1 | //import * as utils from '../utils/utils.js';
2 | import {utils} from '../utils/utils.js';
3 | import * as actions from '../actions/actions.js'
4 |
5 | function isPromise(val) {
6 | return val && typeof val.then === 'function';
7 | }
8 |
9 |
10 | function keep(promise,dispatch, getState){
11 | var pending = getState().router.pending;
12 | pending++;
13 | dispatch(actions.universalSetPeniding(pending));
14 |
15 | promise.then(function(data){
16 | pending = getState().router.pending;
17 | pending--;
18 | setTimeout(()=>{ //dont be mad at this! its seems dirty but it just makes my resolution of the promise happens after the user.
19 | dispatch(actions.universalSetPeniding(pending));
20 | },0)
21 | });
22 | }
23 |
24 | export function universal ({ dispatch, getState }) {
25 |
26 | return (next) => {
27 | return (action) => {
28 | var promiseDone = getState().router.promiseDone;
29 | if (promiseDone === true){ //if the universal router - re-rendered, be done! (stop calls to apis etc...)
30 | return
31 | }
32 | if (isPromise(action)) {
33 | keep(action, dispatch, getState);
34 | } else {
35 | for (var key in action) {
36 | if (isPromise(action[key])) { //needs has own property check
37 | keep(action[key], dispatch, getState);
38 | }
39 | }
40 | }
41 | return next(action);
42 |
43 | }
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/src/reducer/reducer.js:
--------------------------------------------------------------------------------
1 |
2 | function router (state = {},action= {}){
3 |
4 | switch (action.type) {
5 |
6 | case 'PREVENT_NAVIGATION':
7 | return {
8 | ...state,
9 | preventNavigation:true,
10 | preventNavigationMessage:action.message
11 | };
12 |
13 | case 'PREVENTED_NAVIGATION_ATTEMPTED':
14 | return {
15 | ...state,
16 | attemptedOnPrevent:action.url
17 | };
18 |
19 | case 'ALLOW_NAVIGATION':
20 |
21 | delete state.attemptedOnPrevent;
22 | return {
23 | ...state,
24 | preventNavigation:false
25 |
26 | };
27 |
28 | case 'ROUTER_NAVIGATION':
29 |
30 |
31 | var routerObj = action.router;
32 | state.previous = state.url;
33 |
34 | return {
35 | ...state,
36 | ...routerObj
37 | };
38 |
39 |
40 | case 'UNIVERSAL_SET_PENDING':
41 | return {
42 | ...state,
43 | pending:action.val
44 | };
45 |
46 |
47 | case 'UNIVERSAL_PROMISE_DONE':
48 | return {
49 | ...state,
50 | promiseDone:true
51 | };
52 |
53 | case 'UNIVERSAL_SYNC_ACTIONS_DONE':
54 | return {
55 | ...state,
56 | syncActionsDone:true
57 | };
58 |
59 | case 'UNIVERSAL_SYNC_ACTIONS_PENDING':
60 | return {
61 | ...state,
62 | syncActionsDone:false
63 | };
64 |
65 |
66 | default:
67 | return {
68 | ...state
69 | }
70 | }
71 |
72 | }
73 |
74 |
75 | export default {
76 | router:{router}
77 | }
78 |
79 |
--------------------------------------------------------------------------------
/src/redux-tiny-router/redux-tiny-router.js:
--------------------------------------------------------------------------------
1 | var qs = require('query-string');
2 | import * as actions from '../actions/actions.js'
3 | import {utils} from '../utils/utils';
4 | import React from 'react';
5 |
6 | var skipevent = false;
7 | export function init (store) {
8 |
9 | var universal = (typeof __UNIVERSAL__ === 'undefined') ? false : __UNIVERSAL__;
10 | window.__UNIVERSAL__ = universal;
11 | window.__CLIENT__ = true;
12 |
13 | var url = __UNIVERSAL__ ? store.getState().router.url : window.location.pathname + window.location.search;
14 |
15 | store.dispatch(actions.urlChanged(url,'popEvent'));
16 |
17 | window.onbeforeunload = function(e) {
18 | if (store.getState().router.preventNavigation && store.getState().router.preventNavigationMessage.length > 0){
19 | return store.getState().router.preventNavigationMessage
20 | }
21 | };
22 |
23 |
24 | window.onpopstate = function(e){
25 |
26 | if (skipevent) {
27 | skipevent = false;
28 | utils.navindex = e.state || 0; //navindex is necessary as html5 new api did not bless us with information regarding usage of back / forward button
29 | return
30 | }
31 |
32 | let index,navindex,direction,url;
33 | index = e.state || 0;
34 | navindex = utils.navindex;
35 | direction = (index < navindex) ? '_back':'_forward';
36 | url = window.location.pathname + window.location.search;
37 |
38 | if (store.getState().router.preventNavigation){ //if router is preventing navigation
39 | skipevent = true; //we prevent by doing the opposite the user did (and dont want to infinite loop)
40 | (index < navindex) ? history.forward() : history.back();
41 | store.dispatch(actions.urlChanged(direction,'popEvent'));
42 | } else {
43 | store.dispatch(actions.urlChanged(url,'popEvent')); //business as usual
44 | }
45 | utils.navindex = index;
46 | }
47 |
48 |
49 | }
50 |
51 | export function initUniversal (url,createStore,Layout,initialState){
52 |
53 | return new Promise ((resolve,reject) =>{
54 |
55 | global.__CLIENT__ = false;
56 |
57 | initialState = initialState || {};
58 | var store = createStore(initialState,'http://'+url),
59 | state = {},
60 | reRender = false,
61 | rendered = false,
62 | pending,
63 | html;
64 |
65 | store.dispatch(actions.universalSetPeniding(0));
66 |
67 | var unsubscribe = store.subscribe(()=>{
68 | state = store.getState();
69 | var syncActionsDone = state.router.syncActionsDone;
70 | pending = state.router.pending;
71 | if ((pending === 0) && (rendered)){
72 | unsubscribe();
73 | store.dispatch(actions.universalPromiseDone());
74 | if (reRender){
75 | html = React.renderToString();
76 | }
77 | delete state.router.pending;
78 | delete state.router.syncActionsDone;
79 | resolve({html,state});
80 | }
81 |
82 | if ((pending ===0) && (!rendered)){
83 | if (syncActionsDone){
84 |
85 | html = React.renderToString();
86 | rendered = true;
87 | store.dispatch(actions.syncActionsPending());
88 | }
89 | }
90 | if ((rendered) && (pending > 0)){
91 | reRender = true;
92 | }
93 | });
94 |
95 | store.dispatch(actions.urlChanged(url.substring(url.indexOf('/'))));
96 | store.dispatch(actions.syncActionsDone());
97 |
98 | });
99 |
100 | }
101 |
102 |
--------------------------------------------------------------------------------
/src/utils/utils.js:
--------------------------------------------------------------------------------
1 | import * as qs from 'query-string'
2 | import HttpHash from 'http-hash'
3 |
4 | var navindex = 0;
5 | var scrollpos = {};
6 | var hash = HttpHash();
7 |
8 | function getInfo (url,hashObj=hash){
9 | return hashObj.get(url.split('?')[0]);
10 | }
11 |
12 | function set(url){
13 | hash.set(url);
14 | }
15 |
16 | function setRoutes(routes) {
17 | for (let x in routes) {
18 | hash.set(routes[x]);
19 | }
20 | }
21 |
22 | function _doMatching(mapping,url){
23 | let tmpHash = HttpHash();
24 | tmpHash.set(mapping);
25 | var hashResult = getInfo(url,tmpHash);
26 | let router = urlToRouter(url);
27 | router.src = hashResult.src;
28 | router.splat = hashResult.splat;
29 | router.params = hashResult.params;
30 | return router;
31 | }
32 |
33 | function match(mapping,url) {
34 | return _doMatching(mapping,url);
35 | }
36 |
37 | function check(mapping,url) {
38 | return (_doMatching(mapping,url).src);
39 | }
40 |
41 |
42 | function urlToRouter (url){
43 | var path = url.split('?')[0];
44 | var paths = path.split('/');
45 | paths[0] = paths[0] || '/';
46 | var last = (paths[paths.length-1]);
47 | if (last.length < 1) {
48 | paths = paths.splice(0,paths.length-1);
49 | }
50 |
51 | if ((path.charAt(path.length-1) === '/') && (path.length > 1)){
52 | path = path.substr(0,path.length-1); //remove ultimo caracter (o / )
53 | }
54 | var query = qs.parse(url.split('?')[1] || '') ;
55 |
56 | if (query.debug_session){
57 | delete query.debug_session
58 | }
59 |
60 | var hash = getInfo(url);
61 |
62 | return {
63 | url,
64 | src:hash.src,
65 | splat:hash.splat,
66 | params:hash.params,
67 | path,
68 | paths,
69 | query
70 | };
71 |
72 | }
73 |
74 | function toQueryString (path,query){
75 | if (!query){
76 | return path
77 | }
78 | return path + '?'+ qs.stringify(query);
79 | }
80 |
81 | export default {
82 | utils:{
83 | scrollpos,
84 | navindex,
85 | set,
86 | setRoutes,
87 | match,
88 | check,
89 | urlToRouter,
90 | toQueryString
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 |
2 | var path = require('path');
3 |
4 | var reactExternal = {
5 | root: 'React',
6 | commonjs2: 'react',
7 | commonjs: 'react',
8 | amd: 'react'
9 | };
10 |
11 |
12 | var reduxExternal = {
13 | root: 'redux',
14 | commonjs2: 'redux',
15 | commonjs: 'redux',
16 | amd: 'redux'
17 | };
18 |
19 | module.exports = {
20 | //context: __dirname + "/src",
21 | entry: "./src/index.js",
22 | output: {
23 | libraryTarget:'umd',
24 | path: "./bin",
25 | filename: "reduxTinyRouter.js"
26 |
27 | },
28 | externals: {
29 | 'react': reactExternal,
30 | 'redux': reduxExternal
31 | // 'query-string':queryStringExternal
32 | },
33 | // target:'node',
34 |
35 | // exclude:'React',
36 | // devtool:'source-map',
37 | module: {
38 | loaders: [
39 | {
40 | test: /\.jsx?$/,
41 | // loader:'babel?stage=0',
42 | loader:'babel?optional[]=runtime&stage=0',
43 | // loader:'babel?optional[]=runtime&stage=0',
44 | include:[path.resolve('src')],
45 | exclude: /node_modules/
46 | }
47 | ]
48 | }
49 | };
50 |
51 |
--------------------------------------------------------------------------------