├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── dist ├── moon-router.js └── moon-router.min.js ├── gulpfile.js ├── package.json ├── src ├── index.js ├── util │ ├── components.js │ ├── constants.js │ ├── map.js │ ├── run.js │ └── setup.js └── wrapper.js └── test ├── core ├── route │ └── route.js └── util.js ├── karma.conf.js └── test.html /.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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2018 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Moon Router 2 | 3 | A router plugin for Moon. 4 | 5 | [![Build Status](https://travis-ci.org/kbrsh/moon-router.svg?branch=master)](https://travis-ci.org/kbrsh/moon-router) 6 | 7 | ### What? 8 | 9 | Moon router lets you create routes that map to different components. Clicking special links update the view and the URL as if the user is visiting a new page. 10 | 11 | ### Install 12 | 13 | With npm: 14 | 15 | ```bash 16 | $ npm install moon-router 17 | ``` 18 | 19 | ```js 20 | const MoonRouter = require("moon-router"); 21 | Moon.use(MoonRouter) 22 | ``` 23 | 24 | With a CDN/Local: 25 | 26 | ```html 27 | 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 | ##### History Mode 60 | 61 | 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. 62 | 63 | ```js 64 | const router = new MoonRouter({ 65 | default: "/", 66 | map: { 67 | "/": "Root", 68 | "/hello": "Hello" 69 | }, 70 | mode: "history" 71 | }); 72 | ``` 73 | 74 | 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. 75 | 76 | ##### Dynamic Routes 77 | 78 | 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. 79 | 80 | ```js 81 | const router = new MoonRouter({ 82 | map: { 83 | "/:named": "Root", // `named` can be shown with {{route.params.named}} 84 | "/:other/parameter/that/is/:named": "Named", 85 | "/*": "Wildcard" // matches any ONE path 86 | } 87 | }); 88 | ``` 89 | 90 | * Named Parameters are in the `route.params` object 91 | * Query Parameters are in the `route.query` object (`/?key=val`) 92 | 93 | Just remember, to access the special `route` variable, you must state it is a prop in the component, like: 94 | 95 | ```js 96 | Moon.component("Named", { 97 | props: ['route'], 98 | template: '

' 99 | }); 100 | ``` 101 | 102 | #### Define Components 103 | 104 | After initializing Moon Router, define any components referenced. 105 | 106 | ```js 107 | Moon.component("Root", { 108 | template: `
109 |

Welcome to "/"

110 | To /hello 111 |
` 112 | }); 113 | 114 | Moon.component("Hello", { 115 | template: `
116 |

You have Reached "/hello"

117 | Back Home 118 |
` 119 | }); 120 | ``` 121 | 122 | 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`. 123 | 124 | When clicking on this link, the user will be shown the new route at the `router-view` component (see below), and the page will not reload. 125 | 126 | #### Installing Router to Instance 127 | 128 | When creating your Moon instance, call the `init` method on the router inside of the `mounted` hook to bind the instance to the router. 129 | 130 | ```js 131 | new Moon({ 132 | el: "#app", 133 | hooks: { 134 | mounted() { 135 | router.init(this); 136 | } 137 | } 138 | }); 139 | ``` 140 | 141 | ```html 142 |
143 | 144 |
145 | ``` 146 | 147 | This will install the Moon Router to the Moon Instance, and when you visit the page, you will notice the URL changes to `/#/` 148 | 149 | The `router-view` is a `span` with a child component that corresponds with the current route. 150 | 151 | ### License 152 | 153 | Licensed under the [MIT License](https://kbrsh.github.io/license) By [Kabir Shah](https://kabir.ml) 154 | -------------------------------------------------------------------------------- /dist/moon-router.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Moon Router v0.1.3 3 | * Copyright 2016-2018 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 | if(typeof module === "undefined") { 11 | root.MoonRouter = factory(); 12 | } else { 13 | module.exports = factory(); 14 | } 15 | }(this, function() { 16 | var Moon; 17 | 18 | var wildcardAlias = "*"; 19 | var queryAlias = "?"; 20 | var namedParameterAlias = ":"; 21 | var componentAlias = "@"; 22 | 23 | var setup = function (instance, mode) { 24 | var getPath; 25 | var navigate; 26 | var custom = false; 27 | 28 | if(mode === undefined || mode === "hash") { 29 | // Setup Path Getter 30 | getPath = function() { 31 | return window.location.hash.substring(1); 32 | } 33 | 34 | // Create navigation function 35 | navigate = function(route) { 36 | window.location.hash = '#' + route; 37 | run(instance, route); 38 | } 39 | 40 | // Add hash change listener 41 | window.addEventListener("hashchange", function() { 42 | run(instance, instance.getPath()); 43 | }); 44 | } else if(mode === "history") { 45 | // Setup Path Getter 46 | getPath = function() { 47 | return window.location.pathname; 48 | } 49 | 50 | // Create navigation function 51 | navigate = function(route) { 52 | history.pushState(null, null, route); 53 | run(instance, route); 54 | } 55 | 56 | // Create listener 57 | custom = true; 58 | window.addEventListener("popstate", function() { 59 | run(instance, instance.getPath()); 60 | }); 61 | } 62 | 63 | instance.getPath = getPath; 64 | instance.navigate = navigate; 65 | instance.custom = custom; 66 | } 67 | 68 | var registerComponents = function (instance, Moon) { 69 | // Router View component 70 | Moon.extend("router-view", { 71 | data: function() { 72 | return { 73 | component: undefined 74 | } 75 | }, 76 | render: function(m) { 77 | var currentComponent = this.get("component"); 78 | var children; 79 | 80 | if(currentComponent === undefined) { 81 | children = []; 82 | } else { 83 | children = [m(currentComponent, {attrs: {route: instance.route}}, {}, [])]; 84 | } 85 | 86 | return m("span", {}, {}, children); 87 | }, 88 | hooks: { 89 | init: function init() { 90 | instance.components.push(this); 91 | } 92 | } 93 | }); 94 | 95 | // Router Link component 96 | Moon.extend("router-link", { 97 | props: ["to"], 98 | render: function(m) { 99 | var to = this.get("to"); 100 | var attrs = {}; 101 | var meta = {}; 102 | 103 | var same = instance.current === to; 104 | 105 | if(instance.custom === true) { 106 | attrs.href = to; 107 | meta.events = { 108 | "click": [function(event) { 109 | event.preventDefault(); 110 | if(same === false) { 111 | instance.navigate(to); 112 | } 113 | }] 114 | }; 115 | } else { 116 | attrs.href = '#' + to; 117 | } 118 | 119 | if(same === true) { 120 | attrs["class"] = instance.activeClass; 121 | } 122 | 123 | return m('a', {attrs: attrs}, meta, this.insert); 124 | }, 125 | hooks: { 126 | init: function init$1() { 127 | instance.components.push(this); 128 | } 129 | } 130 | }); 131 | } 132 | 133 | var map = function (routes) { 134 | var routesMap = {}; 135 | 136 | for(var route in routes) { 137 | var currentMapState = routesMap; 138 | 139 | // Split up by parts 140 | var parts = route.substring(1).split("/"); 141 | for(var i = 0; i < parts.length; i++) { 142 | var part = parts[i]; 143 | 144 | // Found named parameter 145 | if(part[0] === namedParameterAlias) { 146 | var param = currentMapState[namedParameterAlias]; 147 | if(param === undefined) { 148 | currentMapState[namedParameterAlias] = { 149 | name: part.substring(1) 150 | }; 151 | } else { 152 | param.name = part.substring(1); 153 | } 154 | 155 | currentMapState = currentMapState[namedParameterAlias]; 156 | } else { 157 | // Add part to map 158 | if(currentMapState[part] === undefined) { 159 | currentMapState[part] = {}; 160 | } 161 | 162 | currentMapState = currentMapState[part]; 163 | } 164 | } 165 | 166 | // Add component 167 | currentMapState["@"] = routes[route]; 168 | } 169 | 170 | return routesMap; 171 | } 172 | 173 | var run = function (instance, path) { 174 | // Change current component and build 175 | var parts = path.slice(1).split("/"); 176 | var currentMapState = instance.map; 177 | var context = { 178 | query: {}, 179 | params: {} 180 | } 181 | 182 | for(var i = 0; i < parts.length; i++) { 183 | var part = parts[i]; 184 | 185 | // Query Parameters 186 | if(part.indexOf(queryAlias) !== -1) { 187 | var splitQuery = part.split(queryAlias); 188 | part = splitQuery.shift(); 189 | 190 | for(var j = 0; j < splitQuery.length; j++) { 191 | var keyVal = splitQuery[j].split('='); 192 | context.query[keyVal[0]] = keyVal[1]; 193 | } 194 | } 195 | 196 | if(currentMapState[part] === undefined) { 197 | var namedParameter = currentMapState[namedParameterAlias]; 198 | 199 | if(namedParameter !== undefined) { 200 | // Named Parameter 201 | context.params[namedParameter.name] = part; 202 | part = namedParameterAlias; 203 | } else if(currentMapState[wildcardAlias] !== undefined) { 204 | // Wildcard 205 | part = wildcardAlias; 206 | } 207 | } 208 | 209 | // Move through state 210 | currentMapState = currentMapState[part]; 211 | 212 | // Path not in map 213 | if(currentMapState === undefined) { 214 | run(instance, instance.default); 215 | return false; 216 | } 217 | } 218 | 219 | // Handler not in map 220 | if(currentMapState[componentAlias] === undefined) { 221 | run(instance, instance.default); 222 | return false; 223 | } 224 | 225 | // Setup current information 226 | instance.current = path; 227 | 228 | // Setup Route Context 229 | instance.route = context; 230 | 231 | // Build Instance 232 | instance.instance.build(); 233 | 234 | // Build components 235 | var component = currentMapState[componentAlias]; 236 | var components = instance.components; 237 | for(var i$1 = 0; i$1 < components.length; i$1++) { 238 | components[i$1].set("component", component); 239 | } 240 | 241 | return true; 242 | } 243 | 244 | 245 | function MoonRouter(options) { 246 | // Instance 247 | this.instance = undefined; 248 | 249 | // Default route 250 | var defaultRoute = options["default"]; 251 | if(defaultRoute === undefined) { 252 | this["default"] = "/"; 253 | } else { 254 | this["default"] = defaultRoute; 255 | } 256 | 257 | // Route to component map 258 | var providedMap = options.map; 259 | if(providedMap === undefined) { 260 | this.map = {}; 261 | } else { 262 | this.map = map(providedMap); 263 | } 264 | 265 | // Route context 266 | this.route = {}; 267 | 268 | // Components 269 | this.components = []; 270 | 271 | // Active class 272 | var activeClass = options.activeClass; 273 | if(activeClass === undefined) { 274 | this.activeClass = "router-link-active"; 275 | } else { 276 | this.activeClass = activeClass; 277 | } 278 | 279 | // Register components 280 | registerComponents(this, Moon); 281 | 282 | // Initialize route 283 | setup(this, options.mode); 284 | } 285 | 286 | // Install router to instance 287 | MoonRouter.prototype.init = function(instance) { 288 | this.instance = instance; 289 | this.navigate(this.getPath()); 290 | } 291 | 292 | // Plugin init 293 | MoonRouter.init = function (_Moon) { 294 | Moon = _Moon; 295 | } 296 | 297 | return MoonRouter; 298 | })); 299 | -------------------------------------------------------------------------------- /dist/moon-router.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Moon Router v0.1.3 3 | * Copyright 2016-2018 Kabir Shah 4 | * Released under the MIT License 5 | * https://github.com/kbrsh/moon-router 6 | */ 7 | !function(t,n){"undefined"==typeof module?t.MoonRouter=n():module.exports=n()}(this,function(){function t(t){this.instance=void 0;var i=t.default;void 0===i?this.default="/":this.default=i;var e=t.map;void 0===e?this.map={}:this.map=u(e),this.route={},this.components=[];var o=t.activeClass;void 0===o?this.activeClass="router-link-active":this.activeClass=o,s(this,n),a(this,t.mode)}var n,i="*",e="?",o=":",r="@",a=function(t,n){var i,e,o=!1;void 0===n||"hash"===n?(i=function(){return window.location.hash.substring(1)},e=function(n){window.location.hash="#"+n,c(t,n)},window.addEventListener("hashchange",function(){c(t,t.getPath())})):"history"===n&&(i=function(){return window.location.pathname},e=function(n){history.pushState(null,null,n),c(t,n)},o=!0,window.addEventListener("popstate",function(){c(t,t.getPath())})),t.getPath=i,t.navigate=e,t.custom=o},s=function(t,n){n.extend("router-view",{data:function(){return{component:void 0}},render:function(n){var i,e=this.get("component");return i=void 0===e?[]:[n(e,{attrs:{route:t.route}},{},[])],n("span",{},{},i)},hooks:{init:function(){t.components.push(this)}}}),n.extend("router-link",{props:["to"],render:function(n){var i=this.get("to"),e={},o={},r=t.current===i;return t.custom===!0?(e.href=i,o.events={click:[function(n){n.preventDefault(),r===!1&&t.navigate(i)}]}):e.href="#"+i,r===!0&&(e.class=t.activeClass),n("a",{attrs:e},o,this.insert)},hooks:{init:function(){t.components.push(this)}}})},u=function(t){var n={};for(var i in t){for(var e=n,r=i.substring(1).split("/"),a=0;a { 58 | Moon = _Moon; 59 | } 60 | -------------------------------------------------------------------------------- /src/util/components.js: -------------------------------------------------------------------------------- 1 | const registerComponents = (instance, Moon) => { 2 | // Router View component 3 | Moon.extend("router-view", { 4 | data: function() { 5 | return { 6 | component: undefined 7 | } 8 | }, 9 | render: function(m) { 10 | const currentComponent = this.get("component"); 11 | let children; 12 | 13 | if(currentComponent === undefined) { 14 | children = []; 15 | } else { 16 | children = [m(currentComponent, {attrs: {route: instance.route}}, {}, [])]; 17 | } 18 | 19 | return m("span", {}, {}, children); 20 | }, 21 | hooks: { 22 | init() { 23 | instance.components.push(this); 24 | } 25 | } 26 | }); 27 | 28 | // Router Link component 29 | Moon.extend("router-link", { 30 | props: ["to"], 31 | render: function(m) { 32 | const to = this.get("to"); 33 | let attrs = {}; 34 | let meta = {}; 35 | 36 | const same = instance.current === to; 37 | 38 | if(instance.custom === true) { 39 | attrs.href = to; 40 | meta.events = { 41 | "click": [function(event) { 42 | event.preventDefault(); 43 | if(same === false) { 44 | instance.navigate(to); 45 | } 46 | }] 47 | }; 48 | } else { 49 | attrs.href = '#' + to; 50 | } 51 | 52 | if(same === true) { 53 | attrs["class"] = instance.activeClass; 54 | } 55 | 56 | return m('a', {attrs: attrs}, meta, this.insert); 57 | }, 58 | hooks: { 59 | init() { 60 | instance.components.push(this); 61 | } 62 | } 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /src/util/constants.js: -------------------------------------------------------------------------------- 1 | const wildcardAlias = "*"; 2 | const queryAlias = "?"; 3 | const namedParameterAlias = ":"; 4 | const componentAlias = "@"; 5 | -------------------------------------------------------------------------------- /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.substring(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] === namedParameterAlias) { 14 | let param = currentMapState[namedParameterAlias]; 15 | if(param === undefined) { 16 | currentMapState[namedParameterAlias] = { 17 | name: part.substring(1) 18 | }; 19 | } else { 20 | param.name = part.substring(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 | -------------------------------------------------------------------------------- /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 = currentMapState[namedParameterAlias]; 26 | 27 | if(namedParameter !== undefined) { 28 | // Named Parameter 29 | context.params[namedParameter.name] = part; 30 | part = namedParameterAlias; 31 | } else if(currentMapState[wildcardAlias] !== undefined) { 32 | // Wildcard 33 | part = wildcardAlias; 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 = path; 55 | 56 | // Setup Route Context 57 | instance.route = context; 58 | 59 | // Build Instance 60 | instance.instance.build(); 61 | 62 | // Build components 63 | const component = currentMapState[componentAlias]; 64 | const components = instance.components; 65 | for(let i = 0; i < components.length; i++) { 66 | components[i].set("component", component); 67 | } 68 | 69 | return true; 70 | } 71 | -------------------------------------------------------------------------------- /src/util/setup.js: -------------------------------------------------------------------------------- 1 | const setup = (instance, mode) => { 2 | let getPath; 3 | let navigate; 4 | let custom = false; 5 | 6 | if(mode === undefined || mode === "hash") { 7 | // Setup Path Getter 8 | getPath = function() { 9 | return window.location.hash.substring(1); 10 | } 11 | 12 | // Create navigation function 13 | navigate = function(route) { 14 | window.location.hash = '#' + route; 15 | run(instance, route); 16 | } 17 | 18 | // Add hash change listener 19 | window.addEventListener("hashchange", function() { 20 | run(instance, instance.getPath()); 21 | }); 22 | } else if(mode === "history") { 23 | // Setup Path Getter 24 | getPath = function() { 25 | return window.location.pathname; 26 | } 27 | 28 | // Create navigation function 29 | navigate = function(route) { 30 | history.pushState(null, null, route); 31 | run(instance, route); 32 | } 33 | 34 | // Create listener 35 | custom = true; 36 | window.addEventListener("popstate", function() { 37 | run(instance, instance.getPath()); 38 | }); 39 | } 40 | 41 | instance.getPath = getPath; 42 | instance.navigate = navigate; 43 | instance.custom = custom; 44 | } 45 | -------------------------------------------------------------------------------- /src/wrapper.js: -------------------------------------------------------------------------------- 1 | (function(root, factory) { 2 | /* ======= Global Moon Router ======= */ 3 | if(typeof module === "undefined") { 4 | root.MoonRouter = factory(); 5 | } else { 6 | module.exports = factory(); 7 | } 8 | }(this, function() { 9 | //=require ../dist/moon-router.js 10 | return MoonRouter; 11 | })); 12 | -------------------------------------------------------------------------------- /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 router = new MoonRouter({ 27 | "default": "/context.html", 28 | "map": { 29 | "/context.html": "Root", 30 | "/context.html/test": "Test" 31 | }, 32 | "mode": "history" 33 | }); 34 | 35 | var app = new Moon({ 36 | root: "#history", 37 | hooks: { 38 | mounted: function() { 39 | router.init(this); 40 | } 41 | } 42 | }); 43 | 44 | it("should initialize a router view", function() { 45 | return wait(function() { 46 | expect(el.firstChild.nextSibling.firstChild.nodeName).to.equal("H1"); 47 | expect(el.firstChild.nextSibling.firstChild.innerHTML).to.equal("Root Route Message"); 48 | historyDone[0] = true; 49 | }); 50 | }); 51 | 52 | it("should update with data", function() { 53 | component.set("msg", "Changed"); 54 | return wait(function() { 55 | expect(el.firstChild.nextSibling.firstChild.innerHTML).to.equal("Root Route Changed"); 56 | historyDone[1] = true; 57 | }); 58 | }); 59 | 60 | it("should navigate with router link", function() { 61 | el.firstChild.click(); 62 | return wait(function() { 63 | expect(el.firstChild.nextSibling.firstChild.innerHTML).to.equal("Test Route"); 64 | expect(el.firstChild.getAttribute("class")).to.equal("router-link-active"); 65 | historyDone[2] = true; 66 | }); 67 | }); 68 | 69 | it("should navigate from code", function() { 70 | expect(el.firstChild.getAttribute("class")).to.equal("router-link-active"); 71 | router.navigate("/context.html"); 72 | return wait(function() { 73 | expect(el.firstChild.nextSibling.firstChild.innerHTML).to.equal("Root Route Message"); 74 | expect(el.firstChild.getAttribute("class")).to.equal(null); 75 | historyDone[3] = true; 76 | }); 77 | }); 78 | }); 79 | 80 | describe("Hash Mode", function() { 81 | var el = null; 82 | var component = null; 83 | var router = null 84 | var app = null; 85 | 86 | // Poll to ensure history tests are done 87 | var checkHistory = function(done) { 88 | if(historyDone[0] === true && historyDone[1] === true && historyDone[2] === true && historyDone[3] === true) { 89 | window.removeEventListener("popstate"); 90 | done(); 91 | } else { 92 | setInterval(function() { 93 | checkHistory(done); 94 | }, 500); 95 | } 96 | } 97 | 98 | before(function(done) { 99 | checkHistory(function() { 100 | el = createTestElement("route", ""); 101 | component = null; 102 | 103 | Moon.extend("Root", { 104 | template: "

Root Route {{msg}}

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

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

" 120 | }); 121 | 122 | router = new MoonRouter({ 123 | "default": "/", 124 | "map": { 125 | "/": "Root", 126 | "/test/*/:namedParam": "Test" 127 | } 128 | }); 129 | 130 | app = new Moon({ 131 | root: "#route", 132 | hooks: { 133 | mounted: function() { 134 | router.init(this); 135 | } 136 | } 137 | }); 138 | 139 | done(); 140 | }); 141 | }); 142 | 143 | it("should initialize a router view", function() { 144 | return wait(function() { 145 | expect(el.firstChild.nextSibling.firstChild.nodeName).to.equal("H1"); 146 | expect(el.firstChild.nextSibling.firstChild.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.firstChild.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(null); 159 | el.firstChild.click(); 160 | return wait(function() { 161 | expect(el.firstChild.nextSibling.firstChild.innerHTML).to.equal("Test Route true named"); 162 | expect(el.firstChild.getAttribute("class")).to.equal("router-link-active"); 163 | }); 164 | }); 165 | 166 | it("should navigate from code", function() { 167 | expect(el.firstChild.getAttribute("class")).to.equal("router-link-active"); 168 | router.navigate("/"); 169 | return wait(function() { 170 | expect(el.firstChild.nextSibling.firstChild.innerHTML).to.equal("Root Route Message"); 171 | expect(el.firstChild.getAttribute("class")).to.equal(null); 172 | }); 173 | }); 174 | }); 175 | }); 176 | -------------------------------------------------------------------------------- /test/core/util.js: -------------------------------------------------------------------------------- 1 | Moon.use(MoonRouter); 2 | 3 | var expect = chai.expect; 4 | 5 | if(document.getElementById("els")) { 6 | var els = document.getElementById("els"); 7 | } else { 8 | var els = document.createElement("div"); 9 | els.id = "els"; 10 | document.body.appendChild(els); 11 | } 12 | 13 | var createTestElement = function(id, html) { 14 | var el = document.createElement("div"); 15 | el.setAttribute("m-mask", ""); 16 | el.innerHTML = html; 17 | el.id = id; 18 | els.appendChild(el); 19 | return el; 20 | } 21 | 22 | // Promise Polyfill 23 | !function(e){function n(){}function t(e,n){return function(){e.apply(n,arguments)}}function o(e){if("object"!=typeof this)throw new TypeError("Promises must be constructed via new");if("function"!=typeof e)throw new TypeError("not a function");this._state=0,this._handled=!1,this._value=void 0,this._deferreds=[],s(e,this)}function i(e,n){for(;3===e._state;)e=e._value;return 0===e._state?void e._deferreds.push(n):(e._handled=!0,void o._immediateFn(function(){var t=1===e._state?n.onFulfilled:n.onRejected;if(null===t)return void(1===e._state?r:u)(n.promise,e._value);var o;try{o=t(e._value)}catch(i){return void u(n.promise,i)}r(n.promise,o)}))}function r(e,n){try{if(n===e)throw new TypeError("A promise cannot be resolved with itself.");if(n&&("object"==typeof n||"function"==typeof n)){var i=n.then;if(n instanceof o)return e._state=3,e._value=n,void f(e);if("function"==typeof i)return void s(t(i,n),e)}e._state=1,e._value=n,f(e)}catch(r){u(e,r)}}function u(e,n){e._state=2,e._value=n,f(e)}function f(e){2===e._state&&0===e._deferreds.length&&o._immediateFn(function(){e._handled||o._unhandledRejectionFn(e._value)});for(var n=0,t=e._deferreds.length;n 2 | 3 | 4 | 5 | Moon Router | Test 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 26 | 27 | --------------------------------------------------------------------------------