├── .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 |
--------------------------------------------------------------------------------