├── .gitignore ├── .travis.yml ├── src ├── util │ ├── constants.js │ ├── defineProperty.js │ ├── map.js │ ├── components.js │ ├── setup.js │ └── run.js ├── wrapper.js └── index.js ├── test ├── karma.conf.js ├── core │ ├── intercept.js │ ├── util.js │ └── route │ │ └── route.js └── test.html ├── LICENSE ├── package.json ├── gulpfile.js ├── dist ├── moon-router.min.js └── moon-router.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | script: npm run test 3 | node_js: 4 | - "node" 5 | -------------------------------------------------------------------------------- /src/util/constants.js: -------------------------------------------------------------------------------- 1 | const wildcardAlias = "*"; 2 | const queryAlias = "?"; 3 | const namedParameterAlias = ":"; 4 | const componentAlias = "@"; 5 | -------------------------------------------------------------------------------- /src/util/defineProperty.js: -------------------------------------------------------------------------------- 1 | const defineProperty = function(obj, prop, value, def) { 2 | if(value === undefined) { 3 | obj[prop] = def; 4 | } else { 5 | obj[prop] = value; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/wrapper.js: -------------------------------------------------------------------------------- 1 | (function(root, factory) { 2 | /* ======= Global Moon Router ======= */ 3 | (typeof module === "object" && module.exports) ? module.exports = factory() : root.MoonRouter = factory(); 4 | }(this, function() { 5 | //=require ../dist/moon-router.js 6 | return MoonRouter; 7 | })); 8 | -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | basePath: '', 4 | frameworks: ['mocha'], 5 | 6 | files: [ 7 | './core/intercept.js', 8 | '../dist/moon-router.js', 9 | '../node_modules/moonjs/dist/moon.min.js', 10 | '../node_modules/chai/chai.js', 11 | './core/util.js', 12 | './core/route/*.js' 13 | ], 14 | 15 | exclude: [ 16 | ], 17 | 18 | reporters: ['spec'], 19 | 20 | port: 9876, 21 | 22 | colors: true, 23 | 24 | logLevel: config.LOG_INFO, 25 | 26 | autoWatch: false, 27 | 28 | browsers: ['PhantomJS'], 29 | 30 | singleRun: true, 31 | 32 | concurrency: Infinity 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /test/core/intercept.js: -------------------------------------------------------------------------------- 1 | // Event intercept 2 | window.addEventListenerBase=window.addEventListener,window.addEventListener=function(t,e){this.EventList||(this.EventList=[]),this.addEventListenerBase.apply(this,arguments),this.EventList[t]||(this.EventList[t]=[]);for(var n=this.EventList[t],s=0;s!=n.length;s++)if(n[s]===e)return;n.push(e)},window.removeEventListenerBase=window.removeEventListener,window.removeEventListener=function(t,e){if(this.EventList||(this.EventList=[]),e instanceof Function&&this.removeEventListenerBase.apply(this,arguments),this.EventList[t]){for(var n=this.EventList[t],s=0;s!=n.length;){var i=n[s];if(e){if(i===e){n.splice(s,1);break}s++}else this.removeEventListenerBase(t,i),n.splice(s,1)}0==n.length&&delete this.EventList[t]}}; 3 | -------------------------------------------------------------------------------- /test/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Moon Router | Test 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/util/map.js: -------------------------------------------------------------------------------- 1 | const map = (routes) => { 2 | let routesMap = {}; 3 | 4 | for(let route in routes) { 5 | let currentMapState = routesMap; 6 | 7 | // Split up by Parts 8 | const parts = route.slice(1).split("/"); 9 | for(let i = 0; i < parts.length; i++) { 10 | let part = parts[i]; 11 | 12 | // Found Named Parameter 13 | if(part[0] === ":") { 14 | let param = currentMapState[namedParameterAlias]; 15 | if(param === undefined) { 16 | currentMapState[namedParameterAlias] = { 17 | name: part.slice(1) 18 | }; 19 | } else { 20 | param.name = part.slice(1); 21 | } 22 | 23 | currentMapState = currentMapState[namedParameterAlias]; 24 | } else { 25 | // Add Part to Map 26 | if(currentMapState[part] === undefined) { 27 | currentMapState[part] = {}; 28 | } 29 | 30 | currentMapState = currentMapState[part]; 31 | } 32 | } 33 | 34 | // Add Component 35 | currentMapState["@"] = routes[route]; 36 | } 37 | 38 | return routesMap; 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Kabir Shah 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moon-router", 3 | "version": "0.1.3", 4 | "description": "Router for Moon", 5 | "main": "dist/moon-router.min.js", 6 | "scripts": { 7 | "build": "gulp", 8 | "test": "gulp test" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/kbrsh/moon-router.git" 13 | }, 14 | "keywords": [ 15 | "moon", 16 | "router", 17 | "router", 18 | "route", 19 | "moon" 20 | ], 21 | "author": "Kabir Shah", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/kbrsh/moon-router/issues" 25 | }, 26 | "homepage": "https://github.com/kbrsh/moon-router#readme", 27 | "devDependencies": { 28 | "chai": "^4.1.0", 29 | "gulp": "^3.9.1", 30 | "gulp-buble": "^0.8.0", 31 | "gulp-concat": "^2.6.1", 32 | "gulp-header": "^1.8.8", 33 | "gulp-include": "^2.3.1", 34 | "gulp-replace": "^0.5.4", 35 | "gulp-size": "^2.1.0", 36 | "gulp-uglifyjs": "^0.6.2", 37 | "karma": "^1.7.0", 38 | "karma-mocha": "^1.3.0", 39 | "karma-phantomjs-launcher": "^1.0.4", 40 | "karma-spec-reporter": "0.0.31", 41 | "mocha": "^3.4.2", 42 | "moonjs": "github:kbrsh/moon" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/util/components.js: -------------------------------------------------------------------------------- 1 | const registerComponents = (instance, Moon) => { 2 | // Router View Component 3 | Moon.extend("router-view", { 4 | functional: true, 5 | render: function(m) { 6 | return m(instance.current.component, {attrs: {route: instance.route}}, {dynamic: 1}, []); 7 | } 8 | }); 9 | 10 | // Router Link Component 11 | Moon.extend("router-link", { 12 | functional: true, 13 | render: function(m, state) { 14 | const data = state.data; 15 | const to = data["to"]; 16 | let meta = { 17 | dynamic: 1 18 | }; 19 | 20 | const same = instance.current.path === to; 21 | 22 | if(instance.custom === true) { 23 | data["href"] = instance.base + to; 24 | meta.eventListeners = { 25 | "click": [function(event) { 26 | event.preventDefault(); 27 | if(same === false) { 28 | instance.navigate(to); 29 | } 30 | }] 31 | }; 32 | } else { 33 | data["href"] = `#${to}`; 34 | } 35 | 36 | delete data["to"]; 37 | 38 | if(same === true) { 39 | if(data["class"] === undefined) { 40 | data["class"] = instance.activeClass; 41 | } else { 42 | data["class"] += ` ${instance.activeClass}`; 43 | } 44 | } 45 | 46 | return m('a', {attrs: data}, meta, state.insert); 47 | } 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | let Moon = null; 2 | 3 | //=require ./util/constants.js 4 | //=require ./util/defineProperty.js 5 | //=require ./util/setup.js 6 | //=require ./util/components.js 7 | //=require ./util/map.js 8 | //=require ./util/run.js 9 | 10 | function MoonRouter(options) { 11 | // Moon Instance 12 | this.instance = null; 13 | 14 | // Base 15 | defineProperty(this, "base", options.base, ""); 16 | 17 | // Default Route 18 | defineProperty(this, "default", options["default"], "/"); 19 | 20 | // Route to Component Map 21 | const providedMap = options.map; 22 | if(providedMap === undefined) { 23 | this.map = {}; 24 | } else { 25 | this.map = map(providedMap); 26 | } 27 | 28 | // Route Context 29 | this.route = {}; 30 | 31 | // Active Class 32 | defineProperty(this, "activeClass", options["activeClass"], "router-link-active"); 33 | 34 | // Register Components 35 | registerComponents(this, Moon); 36 | 37 | // Initialize Route 38 | setup(this, options.mode); 39 | } 40 | 41 | // Install MoonRouter to Moon Instance 42 | MoonRouter.prototype.install = function(instance) { 43 | this.instance = instance; 44 | } 45 | 46 | // Init for Plugin 47 | MoonRouter.init = (_Moon) => { 48 | Moon = _Moon; 49 | 50 | // Edit init for Moon to install Moon Router when given as an option 51 | var MoonInit = Moon.prototype.init; 52 | Moon.prototype.init = function() { 53 | if(this.options.router !== undefined) { 54 | this.router = this.options.router; 55 | this.router.install(this); 56 | } 57 | MoonInit.apply(this, arguments); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/util/setup.js: -------------------------------------------------------------------------------- 1 | const setup = (instance, mode) => { 2 | let getPath = null; 3 | let navigate = null; 4 | let custom = false; 5 | 6 | if(mode === undefined) { 7 | // Setup Path Getter 8 | getPath = function() { 9 | let path = window.location.hash.slice(1); 10 | 11 | if(path.length === 0) { 12 | path = "/"; 13 | } 14 | 15 | return path; 16 | } 17 | 18 | // Create navigation function 19 | navigate = function(route) { 20 | window.location.hash = route; 21 | run(instance, route); 22 | } 23 | 24 | // Add hash change listener 25 | window.addEventListener("hashchange", function() { 26 | instance.navigate(instance.getPath()); 27 | }); 28 | } else if(mode === "history") { 29 | // Setup Path Getter 30 | getPath = function() { 31 | let path = window.location.pathname.substring(instance.base.length); 32 | 33 | if(path.length === 0) { 34 | path = "/"; 35 | } 36 | 37 | return path; 38 | } 39 | 40 | // Create navigation function 41 | navigate = function(route) { 42 | history.pushState(null, null, instance.base + route); 43 | run(instance, route); 44 | } 45 | 46 | // Create listener 47 | custom = true; 48 | window.addEventListener("popstate", function() { 49 | run(instance, instance.getPath()); 50 | }); 51 | } 52 | 53 | const initPath = getPath(); 54 | instance.current = { 55 | path: initPath, 56 | component: null 57 | }; 58 | 59 | instance.getPath = getPath; 60 | instance.navigate = navigate; 61 | instance.custom = custom; 62 | 63 | navigate(initPath); 64 | } 65 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | var pkg = require('./package.json'); 5 | var uglify = require("gulp-uglifyjs"); 6 | var buble = require('gulp-buble'); 7 | var replace = require('gulp-replace'); 8 | var include = require("gulp-include"); 9 | var concat = require("gulp-concat"); 10 | var header = require("gulp-header"); 11 | var size = require("gulp-size"); 12 | 13 | var Server = require("karma").Server; 14 | 15 | var comment = `/** 16 | * Moon Router v${pkg.version} 17 | * Copyright 2016-2017 Kabir Shah 18 | * Released under the MIT License 19 | * https://github.com/kbrsh/moon-router 20 | */\r\n`; 21 | 22 | // Build Moon Router 23 | gulp.task('transpile', function () { 24 | return gulp.src(['./src/index.js']) 25 | .pipe(include()) 26 | .pipe(buble()) 27 | .pipe(concat('moon-router.js')) 28 | .pipe(gulp.dest('./dist/')); 29 | }); 30 | 31 | gulp.task('build', ['transpile'], function () { 32 | return gulp.src(['./src/wrapper.js']) 33 | .pipe(include()) 34 | .pipe(concat('moon-router.js')) 35 | .pipe(header(comment + '\n')) 36 | .pipe(replace('__VERSION__', pkg.version)) 37 | .pipe(size()) 38 | .pipe(gulp.dest('./dist/')); 39 | }); 40 | 41 | // Build minified (compressed) version of Moon Router 42 | gulp.task('minify', ['build'], function() { 43 | return gulp.src(['./dist/moon-router.js']) 44 | .pipe(uglify()) 45 | .pipe(header(comment)) 46 | .pipe(size()) 47 | .pipe(size({ 48 | gzip: true 49 | })) 50 | .pipe(concat('moon-router.min.js')) 51 | .pipe(gulp.dest('./dist/')); 52 | }); 53 | 54 | // Run tests 55 | gulp.task('test', function(done) { 56 | new Server({ 57 | configFile: __dirname + '/test/karma.conf.js', 58 | singleRun: true 59 | }, done).start(); 60 | }); 61 | 62 | // Default task 63 | gulp.task('default', ['build', 'minify']); 64 | -------------------------------------------------------------------------------- /src/util/run.js: -------------------------------------------------------------------------------- 1 | const run = (instance, path) => { 2 | // Change Current Component and Build 3 | const parts = path.slice(1).split("/"); 4 | let currentMapState = instance.map; 5 | let context = { 6 | query: {}, 7 | params: {} 8 | } 9 | 10 | for(let i = 0; i < parts.length; i++) { 11 | let part = parts[i]; 12 | 13 | // Query Parameters 14 | if(part.indexOf(queryAlias) !== -1) { 15 | const splitQuery = part.split(queryAlias); 16 | part = splitQuery.shift(); 17 | 18 | for(let j = 0; j < splitQuery.length; j++) { 19 | const keyVal = splitQuery[j].split('='); 20 | context.query[keyVal[0]] = keyVal[1]; 21 | } 22 | } 23 | 24 | if(currentMapState[part] === undefined) { 25 | let namedParameter = null; 26 | 27 | if(currentMapState[wildcardAlias] !== undefined) { 28 | // Wildcard 29 | part = wildcardAlias; 30 | } else if((namedParameter = currentMapState[namedParameterAlias]) !== undefined) { 31 | // Named Parameters 32 | context.params[namedParameter.name] = part; 33 | part = namedParameterAlias; 34 | } 35 | } 36 | 37 | // Move through State 38 | currentMapState = currentMapState[part]; 39 | 40 | // Path Not In Map 41 | if(currentMapState === undefined) { 42 | run(instance, instance.default); 43 | return false; 44 | } 45 | } 46 | 47 | // Handler not in Map 48 | if(currentMapState[componentAlias] === undefined) { 49 | run(instance, instance.default); 50 | return false; 51 | } 52 | 53 | // Setup current information 54 | instance.current = { 55 | path: path, 56 | component: currentMapState[componentAlias] 57 | }; 58 | 59 | // Setup Route Context 60 | instance.route = context; 61 | 62 | // Build Moon Instance 63 | if(instance.instance !== null) { 64 | instance.instance.build(); 65 | } 66 | 67 | return true; 68 | } 69 | -------------------------------------------------------------------------------- /dist/moon-router.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Moon Router v0.1.3 3 | * Copyright 2016-2017 Kabir Shah 4 | * Released under the MIT License 5 | * https://github.com/kbrsh/moon-router 6 | */ 7 | !function(t,n){"object"==typeof module&&module.exports?module.exports=n():t.MoonRouter=n()}(this,function(){function t(t){this.instance=null,r(this,"base",t.base,""),r(this,"default",t.default,"/");var e=t.map;void 0===e?this.map={}:this.map=l(e),this.route={},r(this,"activeClass",t.activeClass,"router-link-active"),u(this,n),s(this,t.mode)}var n=null,e="*",i="?",o=":",a="@",r=function(t,n,e,i){void 0===e?t[n]=i:t[n]=e},s=function(t,n){var e=null,i=null,o=!1;void 0===n?(e=function(){var t=window.location.hash.slice(1);return 0===t.length&&(t="/"),t},i=function(n){window.location.hash=n,c(t,n)},window.addEventListener("hashchange",function(){t.navigate(t.getPath())})):"history"===n&&(e=function(){var n=window.location.pathname.substring(t.base.length);return 0===n.length&&(n="/"),n},i=function(n){history.pushState(null,null,t.base+n),c(t,n)},o=!0,window.addEventListener("popstate",function(){c(t,t.getPath())}));var a=e();t.current={path:a,component:null},t.getPath=e,t.navigate=i,t.custom=o,i(a)},u=function(t,n){n.extend("router-view",{functional:!0,render:function(n){return n(t.current.component,{attrs:{route:t.route}},{dynamic:1},[])}}),n.extend("router-link",{functional:!0,render:function(n,e){var i=e.data,o=i.to,a={dynamic:1},r=t.current.path===o;return t.custom===!0?(i.href=t.base+o,a.eventListeners={click:[function(n){n.preventDefault(),r===!1&&t.navigate(o)}]}):i.href="#"+o,delete i.to,r===!0&&(void 0===i.class?i.class=t.activeClass:i.class+=" "+t.activeClass),n("a",{attrs:i},a,e.insert)}})},l=function(t){var n={};for(var e in t){for(var i=n,a=e.slice(1).split("/"),r=0;r 28 | 31 | ``` 32 | 33 | ### Usage 34 | 35 | Initialize Moon router 36 | 37 | ```js 38 | Moon.use(MoonRouter) 39 | ``` 40 | 41 | #### Creating Routes 42 | 43 | **Before** you create your Moon instance, define your routes like this: 44 | 45 | ```js 46 | const router = new MoonRouter({ 47 | default: "/", 48 | map: { 49 | "/": "Root", 50 | "/hello": "Hello" 51 | } 52 | }); 53 | ``` 54 | 55 | This will map `/` to the `Root` component, and will map `/hello` to the `Hello` component. 56 | 57 | The `default` route is `/`, if a URL is not found, Moon will display this route. 58 | 59 | ##### Base 60 | 61 | If you want routes to be relative to another base, (the default is `""`, meaning the base is `"/"`), you can provide a base. For example: 62 | 63 | ```js 64 | const router = new MoonRouter({ 65 | base: "/app", 66 | default: "/", 67 | map: { 68 | "/": "Root", 69 | "/hello": "Hello" 70 | } 71 | }); 72 | ``` 73 | 74 | This will route `"/app/"` to `"Root"`, and `"/app/hello"` to `"Hello"`. 75 | 76 | ##### History Mode 77 | 78 | Moon Router will use "hash" mode by default, meaning the URL will look something like: `/#/`. If you want routes to look more realistic, you must provide a `mode` option. 79 | 80 | ```js 81 | const router = new MoonRouter({ 82 | default: "/", 83 | map: { 84 | "/": "Root", 85 | "/hello": "Hello" 86 | }, 87 | mode: "history" 88 | }); 89 | ``` 90 | 91 | Still, if a user visits `"/hello"` in history mode, they will get a 404 response. Moon Router can only switch routes in history mode, not initialize them. For this, you must configure your server to always serve a single page but still keep the route. 92 | 93 | ##### Dynamic Routes 94 | 95 | Routes can also be dynamic, with support for query parameters, named parameters, and wildcards. These can be accessed via a `route` prop passed to the view component. 96 | 97 | ```js 98 | const router = new MoonRouter({ 99 | map: { 100 | "/:named": "Root", // `named` can be shown with {{route.params.named}} 101 | "/:other/parameter/that/is/:named": "Named", 102 | "/*": "Wildcard" // matches any ONE path 103 | } 104 | }); 105 | ``` 106 | 107 | * Named Parameters are in the `route.params` object 108 | * Query Parameters are in the `route.query` object (`/?key=val`) 109 | 110 | Just remember, to access the special `route` variable, you must state it is a prop in the component, like: 111 | 112 | ```js 113 | Moon.component("Named", { 114 | props: ['route'], 115 | template: '

' 116 | }); 117 | ``` 118 | 119 | #### Define Components 120 | 121 | After initializing Moon Router, define any components referenced. 122 | 123 | ```js 124 | Moon.component("Root", { 125 | template: `
126 |

Welcome to "/"

127 | To /hello 128 |
` 129 | }); 130 | 131 | Moon.component("Hello", { 132 | template: `
133 |

You have Reached "/hello"

134 | Back Home 135 |
` 136 | }); 137 | ``` 138 | 139 | You will notice the `router-link` component. This is by default, rendered as an `a` tag, and should **always be used** to link to routes. A class of `router-link-active` will be applied to the active link by default, unless another class is provided in `options.activeClass`. 140 | 141 | When clicking on this link, the user will be shown the new route at the `router-view` component (see below), and will not actually be going to a new page. 142 | 143 | #### Installing Router to Instance 144 | 145 | When creating your Moon instance, add the Moon Router instance as the option `router` 146 | 147 | ```js 148 | new Moon({ 149 | el: "#app", 150 | router: router 151 | }); 152 | ``` 153 | 154 | ```html 155 |
156 | 157 |
158 | ``` 159 | 160 | This will install the Moon Router to the Moon Instance, and when you visit the page, you will notice the URL changes to `/#/` 161 | 162 | The `router-view` is a component that will display the current mapped route. 163 | 164 | ### License 165 | 166 | Licensed under the [MIT License](https://kbrsh.github.io/license) By [Kabir Shah](https://kabir.ml) 167 | -------------------------------------------------------------------------------- /test/core/route/route.js: -------------------------------------------------------------------------------- 1 | describe("Route", function() { 2 | var historyDone = [false, false, false, false]; 3 | 4 | describe("History Mode", function() { 5 | var el = createTestElement("history", ""); 6 | var component = null; 7 | 8 | Moon.extend("Root", { 9 | template: "

Root Route {{msg}}

", 10 | data: function() { 11 | return { 12 | msg: "Message" 13 | } 14 | }, 15 | hooks: { 16 | mounted: function() { 17 | component = this; 18 | } 19 | } 20 | }); 21 | 22 | Moon.extend("Test", { 23 | template: "

Test Route

" 24 | }); 25 | 26 | var base = window.location.pathname; 27 | 28 | if(base[base.length - 1] === "/") { 29 | base = base.slice(0, -1); 30 | } 31 | 32 | var router = new MoonRouter({ 33 | default: "/", 34 | map: { 35 | "/": "Root", 36 | "/test": "Test" 37 | }, 38 | mode: "history", 39 | base: base 40 | }); 41 | 42 | var app = new Moon({ 43 | root: "#history", 44 | router: router 45 | }); 46 | 47 | it("should initialize a router view", function() { 48 | return wait(function() { 49 | expect(el.firstChild.nextSibling.nodeName).to.equal("H1"); 50 | expect(el.firstChild.nextSibling.innerHTML).to.equal("Root Route Message"); 51 | historyDone[0] = true; 52 | }); 53 | }); 54 | 55 | it("should update with data", function() { 56 | component.set("msg", "Changed"); 57 | return wait(function() { 58 | expect(el.firstChild.nextSibling.innerHTML).to.equal("Root Route Changed"); 59 | historyDone[1] = true; 60 | }); 61 | }); 62 | 63 | it("should navigate with router link", function() { 64 | expect(el.firstChild.getAttribute("class")).to.equal("router-link-class"); 65 | el.firstChild.click(); 66 | return wait(function() { 67 | expect(el.firstChild.nextSibling.innerHTML).to.equal("Test Route"); 68 | expect(el.firstChild.getAttribute("class")).to.equal("router-link-class router-link-active"); 69 | historyDone[2] = true; 70 | }); 71 | }); 72 | 73 | it("should navigate from code", function() { 74 | expect(el.firstChild.getAttribute("class")).to.equal("router-link-class router-link-active"); 75 | router.navigate("/"); 76 | return wait(function() { 77 | expect(el.firstChild.nextSibling.innerHTML).to.equal("Root Route Message"); 78 | expect(el.firstChild.getAttribute("class")).to.equal("router-link-class"); 79 | historyDone[3] = true; 80 | }); 81 | }); 82 | }); 83 | 84 | describe("Hash Mode", function() { 85 | var el = null; 86 | var component = null; 87 | var router = null 88 | var app = null; 89 | 90 | // Poll to ensure history tests are done 91 | var checkHistory = function(done) { 92 | if(historyDone[0] === true && historyDone[1] === true && historyDone[2] === true && historyDone[3] === true) { 93 | window.removeEventListener("popstate"); 94 | done(); 95 | } else { 96 | setInterval(function() { 97 | checkHistory(done); 98 | }, 500); 99 | } 100 | } 101 | 102 | before(function(done) { 103 | checkHistory(function() { 104 | el = createTestElement("route", ""); 105 | component = null; 106 | 107 | Moon.extend("Root", { 108 | template: "

Root Route {{msg}}

", 109 | data: function() { 110 | return { 111 | msg: "Message" 112 | } 113 | }, 114 | hooks: { 115 | mounted: function() { 116 | component = this; 117 | } 118 | } 119 | }); 120 | 121 | Moon.extend("Test", { 122 | props: ["route"], 123 | template: "

Test Route {{route.query.queryParam}} {{route.params.namedParam}}

" 124 | }); 125 | 126 | router = new MoonRouter({ 127 | default: "/", 128 | map: { 129 | "/": "Root", 130 | "/test/*/:namedParam": "Test" 131 | } 132 | }); 133 | 134 | app = new Moon({ 135 | root: "#route", 136 | router: router 137 | }); 138 | 139 | done(); 140 | }); 141 | }); 142 | 143 | it("should initialize a router view", function() { 144 | return wait(function() { 145 | expect(el.firstChild.nextSibling.nodeName).to.equal("H1"); 146 | expect(el.firstChild.nextSibling.innerHTML).to.equal("Root Route Message"); 147 | }); 148 | }); 149 | 150 | it("should update with data", function() { 151 | component.set("msg", "Changed"); 152 | return wait(function() { 153 | expect(el.firstChild.nextSibling.innerHTML).to.equal("Root Route Changed"); 154 | }); 155 | }); 156 | 157 | it("should navigate with router link", function() { 158 | expect(el.firstChild.getAttribute("class")).to.equal("router-link-class"); 159 | el.firstChild.click(); 160 | return wait(function() { 161 | expect(el.firstChild.nextSibling.innerHTML).to.equal("Test Route true named"); 162 | expect(el.firstChild.getAttribute("class")).to.equal("router-link-class router-link-active"); 163 | }); 164 | }); 165 | 166 | it("should navigate from code", function() { 167 | expect(el.firstChild.getAttribute("class")).to.equal("router-link-class router-link-active"); 168 | router.navigate("/"); 169 | return wait(function() { 170 | expect(el.firstChild.nextSibling.innerHTML).to.equal("Root Route Message"); 171 | expect(el.firstChild.getAttribute("class")).to.equal("router-link-class"); 172 | }); 173 | }); 174 | }); 175 | }); 176 | -------------------------------------------------------------------------------- /dist/moon-router.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Moon Router v0.1.3 3 | * Copyright 2016-2017 Kabir Shah 4 | * Released under the MIT License 5 | * https://github.com/kbrsh/moon-router 6 | */ 7 | 8 | (function(root, factory) { 9 | /* ======= Global Moon Router ======= */ 10 | (typeof module === "object" && module.exports) ? module.exports = factory() : root.MoonRouter = factory(); 11 | }(this, function() { 12 | var Moon = null; 13 | 14 | var wildcardAlias = "*"; 15 | var queryAlias = "?"; 16 | var namedParameterAlias = ":"; 17 | var componentAlias = "@"; 18 | 19 | var defineProperty = function(obj, prop, value, def) { 20 | if(value === undefined) { 21 | obj[prop] = def; 22 | } else { 23 | obj[prop] = value; 24 | } 25 | } 26 | 27 | var setup = function (instance, mode) { 28 | var getPath = null; 29 | var navigate = null; 30 | var custom = false; 31 | 32 | if(mode === undefined) { 33 | // Setup Path Getter 34 | getPath = function() { 35 | var path = window.location.hash.slice(1); 36 | 37 | if(path.length === 0) { 38 | path = "/"; 39 | } 40 | 41 | return path; 42 | } 43 | 44 | // Create navigation function 45 | navigate = function(route) { 46 | window.location.hash = route; 47 | run(instance, route); 48 | } 49 | 50 | // Add hash change listener 51 | window.addEventListener("hashchange", function() { 52 | instance.navigate(instance.getPath()); 53 | }); 54 | } else if(mode === "history") { 55 | // Setup Path Getter 56 | getPath = function() { 57 | var path = window.location.pathname.substring(instance.base.length); 58 | 59 | if(path.length === 0) { 60 | path = "/"; 61 | } 62 | 63 | return path; 64 | } 65 | 66 | // Create navigation function 67 | navigate = function(route) { 68 | history.pushState(null, null, instance.base + route); 69 | run(instance, route); 70 | } 71 | 72 | // Create listener 73 | custom = true; 74 | window.addEventListener("popstate", function() { 75 | run(instance, instance.getPath()); 76 | }); 77 | } 78 | 79 | var initPath = getPath(); 80 | instance.current = { 81 | path: initPath, 82 | component: null 83 | }; 84 | 85 | instance.getPath = getPath; 86 | instance.navigate = navigate; 87 | instance.custom = custom; 88 | 89 | navigate(initPath); 90 | } 91 | 92 | var registerComponents = function (instance, Moon) { 93 | // Router View Component 94 | Moon.extend("router-view", { 95 | functional: true, 96 | render: function(m) { 97 | return m(instance.current.component, {attrs: {route: instance.route}}, {dynamic: 1}, []); 98 | } 99 | }); 100 | 101 | // Router Link Component 102 | Moon.extend("router-link", { 103 | functional: true, 104 | render: function(m, state) { 105 | var data = state.data; 106 | var to = data["to"]; 107 | var meta = { 108 | dynamic: 1 109 | }; 110 | 111 | var same = instance.current.path === to; 112 | 113 | if(instance.custom === true) { 114 | data["href"] = instance.base + to; 115 | meta.eventListeners = { 116 | "click": [function(event) { 117 | event.preventDefault(); 118 | if(same === false) { 119 | instance.navigate(to); 120 | } 121 | }] 122 | }; 123 | } else { 124 | data["href"] = "#" + to; 125 | } 126 | 127 | delete data["to"]; 128 | 129 | if(same === true) { 130 | if(data["class"] === undefined) { 131 | data["class"] = instance.activeClass; 132 | } else { 133 | data["class"] += " " + (instance.activeClass); 134 | } 135 | } 136 | 137 | return m('a', {attrs: data}, meta, state.insert); 138 | } 139 | }); 140 | } 141 | 142 | var map = function (routes) { 143 | var routesMap = {}; 144 | 145 | for(var route in routes) { 146 | var currentMapState = routesMap; 147 | 148 | // Split up by Parts 149 | var parts = route.slice(1).split("/"); 150 | for(var i = 0; i < parts.length; i++) { 151 | var part = parts[i]; 152 | 153 | // Found Named Parameter 154 | if(part[0] === ":") { 155 | var param = currentMapState[namedParameterAlias]; 156 | if(param === undefined) { 157 | currentMapState[namedParameterAlias] = { 158 | name: part.slice(1) 159 | }; 160 | } else { 161 | param.name = part.slice(1); 162 | } 163 | 164 | currentMapState = currentMapState[namedParameterAlias]; 165 | } else { 166 | // Add Part to Map 167 | if(currentMapState[part] === undefined) { 168 | currentMapState[part] = {}; 169 | } 170 | 171 | currentMapState = currentMapState[part]; 172 | } 173 | } 174 | 175 | // Add Component 176 | currentMapState["@"] = routes[route]; 177 | } 178 | 179 | return routesMap; 180 | } 181 | 182 | var run = function (instance, path) { 183 | // Change Current Component and Build 184 | var parts = path.slice(1).split("/"); 185 | var currentMapState = instance.map; 186 | var context = { 187 | query: {}, 188 | params: {} 189 | } 190 | 191 | for(var i = 0; i < parts.length; i++) { 192 | var part = parts[i]; 193 | 194 | // Query Parameters 195 | if(part.indexOf(queryAlias) !== -1) { 196 | var splitQuery = part.split(queryAlias); 197 | part = splitQuery.shift(); 198 | 199 | for(var j = 0; j < splitQuery.length; j++) { 200 | var keyVal = splitQuery[j].split('='); 201 | context.query[keyVal[0]] = keyVal[1]; 202 | } 203 | } 204 | 205 | if(currentMapState[part] === undefined) { 206 | var namedParameter = null; 207 | 208 | if(currentMapState[wildcardAlias] !== undefined) { 209 | // Wildcard 210 | part = wildcardAlias; 211 | } else if((namedParameter = currentMapState[namedParameterAlias]) !== undefined) { 212 | // Named Parameters 213 | context.params[namedParameter.name] = part; 214 | part = namedParameterAlias; 215 | } 216 | } 217 | 218 | // Move through State 219 | currentMapState = currentMapState[part]; 220 | 221 | // Path Not In Map 222 | if(currentMapState === undefined) { 223 | run(instance, instance.default); 224 | return false; 225 | } 226 | } 227 | 228 | // Handler not in Map 229 | if(currentMapState[componentAlias] === undefined) { 230 | run(instance, instance.default); 231 | return false; 232 | } 233 | 234 | // Setup current information 235 | instance.current = { 236 | path: path, 237 | component: currentMapState[componentAlias] 238 | }; 239 | 240 | // Setup Route Context 241 | instance.route = context; 242 | 243 | // Build Moon Instance 244 | if(instance.instance !== null) { 245 | instance.instance.build(); 246 | } 247 | 248 | return true; 249 | } 250 | 251 | 252 | function MoonRouter(options) { 253 | // Moon Instance 254 | this.instance = null; 255 | 256 | // Base 257 | defineProperty(this, "base", options.base, ""); 258 | 259 | // Default Route 260 | defineProperty(this, "default", options["default"], "/"); 261 | 262 | // Route to Component Map 263 | var providedMap = options.map; 264 | if(providedMap === undefined) { 265 | this.map = {}; 266 | } else { 267 | this.map = map(providedMap); 268 | } 269 | 270 | // Route Context 271 | this.route = {}; 272 | 273 | // Active Class 274 | defineProperty(this, "activeClass", options["activeClass"], "router-link-active"); 275 | 276 | // Register Components 277 | registerComponents(this, Moon); 278 | 279 | // Initialize Route 280 | setup(this, options.mode); 281 | } 282 | 283 | // Install MoonRouter to Moon Instance 284 | MoonRouter.prototype.install = function(instance) { 285 | this.instance = instance; 286 | } 287 | 288 | // Init for Plugin 289 | MoonRouter.init = function (_Moon) { 290 | Moon = _Moon; 291 | 292 | // Edit init for Moon to install Moon Router when given as an option 293 | var MoonInit = Moon.prototype.init; 294 | Moon.prototype.init = function() { 295 | if(this.options.router !== undefined) { 296 | this.router = this.options.router; 297 | this.router.install(this); 298 | } 299 | MoonInit.apply(this, arguments); 300 | } 301 | } 302 | 303 | return MoonRouter; 304 | })); 305 | --------------------------------------------------------------------------------