├── .gitignore ├── .idea ├── .name ├── encodings.xml ├── inspectionProfiles │ ├── Project_Default.xml │ └── profiles_settings.xml ├── jsLibraryMappings.xml ├── libraries │ ├── Generated_files.xml │ └── fossnote_node_modules.xml ├── modules.xml ├── redux-tiny-router.iml ├── scopes │ └── scope_settings.xml ├── vcs.xml ├── watcherTasks.xml └── workspace.xml ├── CHANGELOG.md ├── README.md ├── package.json ├── src ├── actions │ └── actions.js ├── components │ ├── link.jsx │ ├── route.jsx │ └── router.jsx ├── enhancer │ └── enhancer.js ├── index.js ├── middleware │ ├── middleware.js │ └── universal.js ├── reducer │ └── reducer.js ├── redux-tiny-router │ └── redux-tiny-router.js └── utils │ └── utils.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source # 2 | ################### 3 | *.com 4 | *.class 5 | *.dll 6 | *.exe 7 | *.o 8 | *.so 9 | 10 | # Packages # 11 | ############ 12 | # it's better to unpack these files and commit the raw source 13 | # git has its own built in compression methods 14 | *.7z 15 | *.dmg 16 | *.gz 17 | *.iso 18 | *.jar 19 | *.rar 20 | *.tar 21 | *.zip 22 | 23 | # Logs and databases # 24 | ###################### 25 | *.log 26 | *.sql 27 | *.sqlite 28 | 29 | # OS generated files # 30 | ###################### 31 | .DS_Store 32 | .DS_Store? 33 | ._* 34 | .Spotlight-V100 35 | .Trashes 36 | ehthumbs.db 37 | Thumbs.db -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | redux-tiny-router -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /.idea/jsLibraryMappings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/libraries/Generated_files.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/libraries/fossnote_node_modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/redux-tiny-router.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.idea/scopes/scope_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/watcherTasks.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 22 | 29 | 30 | -------------------------------------------------------------------------------- /.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 19 | 20 | 21 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 137 | 138 | 193 | 194 | 195 | /usr/bin/bower 196 | $PROJECT_DIR$/bower.json 197 | 198 | 199 | 200 | 201 | true 202 | 203 | 204 | $PROJECT_DIR$/../react/play/node_modules/karma 205 | 206 | 207 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | Assignment issuesJavaScript 221 | 222 | 223 | Bitwise operation issuesJavaScript 224 | 225 | 226 | Code quality toolsJavaScript 227 | 228 | 229 | Code style issuesJavaScript 230 | 231 | 232 | Control flow issuesJavaScript 233 | 234 | 235 | DOM issuesJavaScript 236 | 237 | 238 | Data flow issuesJavaScript 239 | 240 | 241 | Error handlingJavaScript 242 | 243 | 244 | GeneralJavaScript 245 | 246 | 247 | JavaScript 248 | 249 | 250 | JavaScript function metricsJavaScript 251 | 252 | 253 | JavaScript validity issuesJavaScript 254 | 255 | 256 | Naming conventionsJavaScript 257 | 258 | 259 | Node.jsJavaScript 260 | 261 | 262 | Potentially confusing code constructsJavaScript 263 | 264 | 265 | Probable bugsJavaScript 266 | 267 | 268 | TypeScript 269 | 270 | 271 | Unit testingJavaScript 272 | 273 | 274 | 275 | 276 | CoffeeScript 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 315 | 316 | 317 | 318 | 321 | 322 | 325 | 326 | 327 | 328 | 331 | 332 | 335 | 336 | 339 | 340 | 341 | 342 | 345 | 346 | 349 | 350 | 353 | 354 | 357 | 358 | 359 | 360 | 363 | 364 | 367 | 368 | 371 | 372 | 375 | 376 | 377 | 378 | 381 | 382 | 385 | 386 | 389 | 390 | 393 | 394 | 395 | 396 | 399 | 400 | 403 | 404 | 407 | 408 | 411 | 412 | 413 | 414 | 417 | 418 | 421 | 422 | 425 | 426 | 429 | 430 | 431 | 432 | 435 | 436 | 439 | 440 | 443 | 444 | 447 | 448 | 449 | 450 | 453 | 454 | 457 | 458 | 461 | 462 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 546 | 547 | 548 | 549 | 550 | 551 | true 552 | 553 | 554 | 555 | 556 | 557 | $PROJECT_DIR$ 558 | true 559 | 560 | bdd 561 | 562 | DIRECTORY 563 | 564 | false 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 590 | 591 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 1414121226405 607 | 610 | 611 | 1440221866351 612 | 616 | 617 | 1440221930539 618 | 622 | 623 | 1440223381810 624 | 628 | 629 | 1440223741605 630 | 634 | 635 | 1440226669985 636 | 640 | 641 | 1440386874079 642 | 646 | 647 | 1440398421152 648 | 652 | 653 | 1440476042111 654 | 658 | 659 | 1440486087063 660 | 664 | 665 | 1440585994846 666 | 670 | 671 | 1440752721069 672 | 676 | 677 | 1440753787075 678 | 682 | 683 | 1440845617056 684 | 688 | 689 | 1440909485113 690 | 694 | 695 | 1441130785484 696 | 700 | 701 | 1441228421135 702 | 706 | 707 | 1441229242754 708 | 712 | 713 | 1441230165679 714 | 718 | 719 | 1441230254119 720 | 724 | 725 | 1441230328236 726 | 730 | 731 | 1441358902115 732 | 736 | 737 | 1441358991199 738 | 742 | 743 | 1441359321212 744 | 748 | 749 | 1441359735741 750 | 754 | 755 | 1441361265933 756 | 760 | 761 | 1441361371143 762 | 766 | 767 | 1441361490427 768 | 772 | 773 | 1441361678397 774 | 778 | 779 | 1441361876860 780 | 784 | 785 | 1441362291212 786 | 790 | 791 | 1441362820178 792 | 796 | 797 | 1441433944426 798 | 802 | 803 | 1441441456275 804 | 808 | 809 | 1441449336546 810 | 814 | 815 | 1441577054297 816 | 820 | 821 | 1441583373486 822 | 826 | 827 | 1441615645326 828 | 832 | 833 | 1441759413015 834 | 838 | 839 | 1441759468115 840 | 844 | 845 | 1441759538114 846 | 850 | 851 | 1441759569888 852 | 856 | 857 | 1441761011518 858 | 862 | 863 | 1441762243062 864 | 868 | 869 | 1441762255687 870 | 874 | 875 | 1442062104555 876 | 880 | 881 | 1442062352358 882 | 886 | 887 | 1442062370436 888 | 892 | 893 | 1442200160869 894 | 898 | 899 | 1442200185527 900 | 904 | 907 | 908 | 909 | 910 | 911 | 912 | 913 | 914 | 915 | 916 | 917 | 918 | 919 | 920 | 921 | 922 | 923 | 924 | 925 | 926 | 927 | 928 | 929 | 930 | 931 | 932 | 933 | 934 | 935 | 936 | 937 | 938 | 939 | 940 | 941 | 942 | 945 | 948 | 949 | 950 | 952 | 953 | 954 | 955 | 956 | 957 | 958 | 959 | 960 | 961 | 962 | 963 | 964 | 965 | 966 | 967 | 968 | 969 | 970 | 971 | 972 | 973 | 974 | 975 | 977 | 978 | 979 | 981 | 982 | 983 | 984 | 985 | 986 | 987 | 988 | 989 | 990 | 991 | 992 | 993 | 994 | 997 | 998 | 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 | --------------------------------------------------------------------------------