37 |
38 |
39 | )
40 | }
41 | }
42 |
43 | function onclick () {
44 | emit('increment', 1)
45 | }
46 | }
47 |
48 | function countStore (state, emitter) {
49 | state.count = 0
50 | emitter.on('increment', function (count) {
51 | state.count += count
52 | emitter.emit('render')
53 | })
54 | }
55 |
--------------------------------------------------------------------------------
/examples/with-vue-jsx/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "with-vue",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "budo index.js -l -P -- -t [ babelify --presets [ @vue/babel-preset-jsx ] ]"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "devDependencies": {
13 | "@babel/core": "^7.1.5",
14 | "@vue/babel-preset-jsx": "^0.1.0",
15 | "babelify": "^10.0.0",
16 | "budo": "^11.5.0"
17 | },
18 | "dependencies": {
19 | "choo-devtools": "^2.5.1",
20 | "vue": "^2.5.17"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | var scrollToAnchor = require('scroll-to-anchor')
2 | var documentReady = require('document-ready')
3 | var nanotiming = require('nanotiming')
4 | var nanorouter = require('nanorouter')
5 | var nanoquery = require('nanoquery')
6 | var nanohref = require('nanohref')
7 | var nanoraf = require('nanoraf')
8 | var nanobus = require('nanobus')
9 | var assert = require('assert')
10 | var xtend = require('xtend')
11 |
12 | module.exports = Monoapp
13 |
14 | var HISTORY_OBJECT = {}
15 |
16 | function Monoapp (opts) {
17 | if (!(this instanceof Monoapp)) return new Monoapp(opts)
18 | opts = opts || {}
19 |
20 | assert.equal(typeof opts, 'object', 'choo: opts should be type object')
21 |
22 | var self = this
23 |
24 | // define events used by choo
25 | this._events = {
26 | DOMCONTENTLOADED: 'DOMContentLoaded',
27 | DOMTITLECHANGE: 'DOMTitleChange',
28 | REPLACESTATE: 'replaceState',
29 | PUSHSTATE: 'pushState',
30 | NAVIGATE: 'navigate',
31 | POPSTATE: 'popState',
32 | RENDER: 'render'
33 | }
34 |
35 | // properties for internal use only
36 | this._historyEnabled = opts.history === undefined ? true : opts.history
37 | this._hrefEnabled = opts.href === undefined ? true : opts.href
38 | this._hashEnabled = opts.hash === undefined ? true : opts.hash
39 | this._hasWindow = typeof window !== 'undefined'
40 | this._loaded = false
41 | this._stores = []
42 | this._tree = null
43 | this._treeref = null
44 | this._mount = opts.mount
45 | this._render = opts.render
46 | this._toString = opts.toString
47 |
48 | // state
49 | var _state = {
50 | events: this._events,
51 | components: {}
52 | }
53 | if (this._hasWindow) {
54 | this.state = window.initialState
55 | ? xtend(window.initialState, _state)
56 | : _state
57 | delete window.initialState
58 | } else {
59 | this.state = _state
60 | }
61 |
62 | // properties that are part of the API
63 | this.router = nanorouter({ curry: true })
64 | this.emitter = nanobus('choo.emit')
65 | this.emit = this.emitter.emit.bind(this.emitter)
66 |
67 | // listen for title changes; available even when calling .toString()
68 | if (this._hasWindow) this.state.title = document.title
69 | this.emitter.prependListener(this._events.DOMTITLECHANGE, function (title) {
70 | assert.equal(typeof title, 'string', 'events.DOMTitleChange: title should be type string')
71 | self.state.title = title
72 | if (self._hasWindow) document.title = title
73 | })
74 | }
75 |
76 | Monoapp.prototype.route = function (route, handler) {
77 | assert.equal(typeof route, 'string', 'choo.route: route should be type string')
78 | assert.equal(typeof handler, 'function', 'choo.handler: route should be type function')
79 | this.router.on(route, handler)
80 | }
81 |
82 | Monoapp.prototype.use = function (cb) {
83 | assert.equal(typeof cb, 'function', 'choo.use: cb should be type function')
84 | var self = this
85 | this._stores.push(function (state) {
86 | var msg = 'choo.use'
87 | msg = cb.storeName ? msg + '(' + cb.storeName + ')' : msg
88 | var endTiming = nanotiming(msg)
89 | cb(state, self.emitter, self)
90 | endTiming()
91 | })
92 | }
93 |
94 | Monoapp.prototype.start = function () {
95 | assert.equal(typeof window, 'object', 'choo.start: window was not found. .start() must be called in a browser, use .toString() if running in Node')
96 |
97 | var self = this
98 | if (this._historyEnabled) {
99 | this.emitter.prependListener(this._events.NAVIGATE, function () {
100 | self._matchRoute()
101 | if (self._loaded) {
102 | self.emitter.emit(self._events.RENDER)
103 | setTimeout(scrollToAnchor.bind(null, window.location.hash), 0)
104 | }
105 | })
106 |
107 | this.emitter.prependListener(this._events.POPSTATE, function () {
108 | self.emitter.emit(self._events.NAVIGATE)
109 | })
110 |
111 | this.emitter.prependListener(this._events.PUSHSTATE, function (href) {
112 | assert.equal(typeof href, 'string', 'events.pushState: href should be type string')
113 | window.history.pushState(HISTORY_OBJECT, null, href)
114 | self.emitter.emit(self._events.NAVIGATE)
115 | })
116 |
117 | this.emitter.prependListener(this._events.REPLACESTATE, function (href) {
118 | assert.equal(typeof href, 'string', 'events.replaceState: href should be type string')
119 | window.history.replaceState(HISTORY_OBJECT, null, href)
120 | self.emitter.emit(self._events.NAVIGATE)
121 | })
122 |
123 | window.onpopstate = function () {
124 | self.emitter.emit(self._events.POPSTATE)
125 | }
126 |
127 | if (self._hrefEnabled) {
128 | nanohref(function (location) {
129 | var href = location.href
130 | var hash = location.hash
131 | if (href === window.location.href) {
132 | if (!self._hashEnabled && hash) scrollToAnchor(hash)
133 | return
134 | }
135 | self.emitter.emit(self._events.PUSHSTATE, href)
136 | })
137 | }
138 | }
139 |
140 | this._stores.forEach(function (initStore) {
141 | initStore(self.state)
142 | })
143 |
144 | this._matchRoute()
145 | this._tree = this._prerender(this.state)
146 | assert.ok(this._tree, 'choo.start: no valid DOM node returned for location ' + this.state.href)
147 | assert.equal(typeof this._mount, 'function', 'choo: choo._mount should be a function')
148 | assert.equal(typeof this._render, 'function', 'choo: choo._render should be a function')
149 |
150 | this.emitter.prependListener(self._events.RENDER, nanoraf(function () {
151 | var renderTiming = nanotiming('choo.render')
152 | var newTree = self._prerender(self.state)
153 | assert.ok(newTree, 'choo.render: no valid DOM node returned for location ' + self.state.href)
154 |
155 | var morphTiming = nanotiming('choo.morph')
156 | self._treeref = self._render(self._tree, newTree, self._treeref)
157 | morphTiming()
158 |
159 | renderTiming()
160 | }))
161 |
162 | documentReady(function () {
163 | self.emitter.emit(self._events.DOMCONTENTLOADED)
164 | self._loaded = true
165 | })
166 |
167 | return this._tree
168 | }
169 |
170 | Monoapp.prototype.mount = function mount (selector) {
171 | if (typeof window !== 'object') {
172 | assert.ok(typeof selector === 'string', 'choo.mount: selector should be type String')
173 | this.selector = selector
174 | return this
175 | }
176 |
177 | assert.ok(typeof selector === 'string' || typeof selector === 'object', 'choo.mount: selector should be type String or HTMLElement')
178 |
179 | var self = this
180 |
181 | documentReady(function () {
182 | var renderTiming = nanotiming('choo.render')
183 | var newTree = self.start()
184 | if (typeof selector === 'string') {
185 | self._tree = document.querySelector(selector)
186 | } else {
187 | self._tree = selector
188 | }
189 |
190 | assert.ok(self._tree, 'choo.mount: could not query selector: ' + selector)
191 |
192 | var morphTiming = nanotiming('choo.morph')
193 | self._treeref = self._mount(self._tree, newTree, self._tree.lastChild)
194 | morphTiming()
195 |
196 | renderTiming()
197 | })
198 | }
199 |
200 | Monoapp.prototype.toString = function (location, state) {
201 | this.state = xtend(this.state, state || {})
202 |
203 | assert.notEqual(typeof window, 'object', 'choo.mount: window was found. .toString() must be called in Node, use .start() or .mount() if running in the browser')
204 | assert.equal(typeof location, 'string', 'choo.toString: location should be type string')
205 | assert.equal(typeof this.state, 'object', 'choo.toString: state should be type object')
206 | assert.equal(typeof this._toString, 'function', 'choo: choo._toString should be a function')
207 |
208 | var self = this
209 | this._stores.forEach(function (initStore) {
210 | initStore(self.state)
211 | })
212 |
213 | this._matchRoute(location)
214 | var html = this._prerender(this.state)
215 | assert.ok(html, 'choo.toString: no valid value returned for the route ' + location)
216 | assert(!Array.isArray(html), 'choo.toString: return value was an array for the route ' + location)
217 | return typeof html.outerHTML === 'string' ? html.outerHTML : this._toString(html)
218 | }
219 |
220 | Monoapp.prototype._matchRoute = function (locationOverride) {
221 | var location, queryString
222 | if (locationOverride) {
223 | location = locationOverride.replace(/\?.+$/, '').replace(/\/$/, '')
224 | if (!this._hashEnabled) location = location.replace(/#.+$/, '')
225 | queryString = locationOverride
226 | } else {
227 | location = window.location.pathname.replace(/\/$/, '')
228 | if (this._hashEnabled) location += window.location.hash.replace(/^#/, '/')
229 | queryString = window.location.search
230 | }
231 | var matched = this.router.match(location)
232 | this._handler = matched.cb
233 | this.state.href = location
234 | this.state.query = nanoquery(queryString)
235 | this.state.route = matched.route
236 | this.state.params = matched.params
237 | return this.state
238 | }
239 |
240 | Monoapp.prototype._prerender = function (state) {
241 | var routeTiming = nanotiming("choo.prerender('" + state.route + "')")
242 | var res = this._handler(state, this.emit)
243 | routeTiming()
244 | return res
245 | }
246 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "monoapp",
3 | "version": "3.0.0",
4 | "description": "choo architecture without a renderer",
5 | "main": "index.js",
6 | "scripts": {
7 | "build": "mkdir -p dist/ && browserify index -s Monoapp -p bundle-collapser/plugin > dist/bundle.js && browserify index -s Monoapp -p tinyify > dist/bundle.min.js && cat dist/bundle.min.js | gzip --best --stdout | wc -c | pretty-bytes",
8 | "inspect": "browserify --full-paths index -p tinyify | discify --open",
9 | "prepublish": "npm run build",
10 | "deps": "dependency-check . && dependency-check . --extra --no-dev",
11 | "test": "standard --fix && standard && npm run deps"
12 | },
13 | "repository": "jongacnik/monoapp",
14 | "keywords": [
15 | "choo",
16 | "client",
17 | "frontend",
18 | "framework",
19 | "minimal",
20 | "composable",
21 | "tiny"
22 | ],
23 | "author": "Jon Gacnik ",
24 | "license": "MIT",
25 | "dependencies": {
26 | "document-ready": "^2.0.1",
27 | "nanobus": "^4.2.0",
28 | "nanohref": "^3.0.0",
29 | "nanoquery": "^1.1.0",
30 | "nanoraf": "^3.0.0",
31 | "nanorouter": "^4.0.0",
32 | "nanotiming": "^7.0.0",
33 | "scroll-to-anchor": "^1.0.0",
34 | "xtend": "^4.0.1"
35 | },
36 | "devDependencies": {
37 | "browserify": "^16.2.3",
38 | "bundle-collapser": "^1.3.0",
39 | "dependency-check": "^4.0.0",
40 | "discify": "^1.6.3",
41 | "pretty-bytes-cli": "^2.0.0",
42 | "spok": "^1.0.0",
43 | "standard": "^13.0.1",
44 | "tape": "^5.0.0",
45 | "tinyify": "^2.4.3",
46 | "uglifyify": "^5.0.1",
47 | "uglifyjs": "^2.4.11",
48 | "unassertify": "^2.1.1"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # monoapp
2 |
3 | [](https://nodejs.org/api/documentation.html#documentation_stability_index)
4 | [](https://npmjs.org/package/monoapp)
5 | [](https://standardjs.com)
6 | 
7 |
8 | [choo](https://github.com/choojs/choo) architecture without a renderer. Bring-your-own view layer.
9 |
10 | ## Overview
11 |
12 | `monoapp` is an opinionated fork of `choo`, a small frontend framework with a simple, functional architecture. Read-up on the [choo documentation](https://github.com/choojs/choo#api) for details on routing, events, and the architecture in general.
13 |
14 | In `monoapp`, we have removed the modules used to render the dom ([nanohtml](https://github.com/choojs/nanohtml) and [nanomorph](https://github.com/choojs/nanomorph)), and made these pluggable instead. This allows us to build apps using `choo` architecture, but render views and components however we would like. See the [examples directory](https://github.com/jongacnik/monoapp/tree/master/examples/) for using with [react](https://github.com/jongacnik/monoapp/tree/master/examples/with-react), [lit-html](https://github.com/jongacnik/monoapp/tree/master/examples/with-lit-html), [vue](https://github.com/jongacnik/monoapp/tree/master/examples/with-vue-jsx), [nanomorph](https://github.com/jongacnik/monoapp/tree/master/examples/with-nanomorph), etc.
15 |
16 | ## Example
17 |
18 | Clone of the [choo example](https://github.com/choojs/choo#example), but we bring [nanohtml](https://github.com/choojs/nanohtml) and [nanomorph](https://github.com/choojs/nanomorph) ourselves.
19 |
20 | ```js
21 | var html = require('nanohtml')
22 | var morph = require('nanomorph')
23 | var monoapp = require('monoapp')
24 | var devtools = require('choo-devtools')
25 |
26 | var app = monoapp({
27 | mount: morph,
28 | render: morph
29 | })
30 |
31 | app.use(devtools())
32 | app.use(countStore)
33 | app.route('/', mainView)
34 | app.mount('body')
35 |
36 | function mainView (state, emit) {
37 | return html`
38 |
39 |
count is ${state.count}
40 |
41 |
42 | `
43 |
44 | function onclick () {
45 | emit('increment', 1)
46 | }
47 | }
48 |
49 | function countStore (state, emitter) {
50 | state.count = 0
51 | emitter.on('increment', function (count) {
52 | state.count += count
53 | emitter.emit('render')
54 | })
55 | }
56 | ```
57 |
58 | You could also choose to define `mount` and `render` using a simple plugin, rather than passing as options:
59 |
60 | ```js
61 | app.use(withNanomorph)
62 |
63 | function withNanomorph (state, emitter, app) {
64 | app._mount = morph
65 | app._render = morph
66 | }
67 | ```
68 |
69 | ## API
70 |
71 | The only items documented here are the methods to implement. These can be defined as options when creating a `monoapp` instance, or can be set with a plugin. Refer to the [choo documentation](https://github.com/choojs/choo#api) for anything related to app architecture (routing, state, and events).
72 |
73 | ### `app._mount(tree, newTree, root)`*
74 |
75 | Mount tree onto the root:
76 |
77 | ```js
78 | app._mount = (tree, newTree, root) => nanomorph(tree, newTree)
79 | ```
80 |
81 | ### `app._render(tree, newTree, root)`*
82 |
83 | Render new tree:
84 |
85 | ```js
86 | app._render = (tree, newTree, root) => nanomorph(tree, newTree)
87 | ```
88 |
89 | ### `app._toString(tree)`
90 |
91 | Convert tree to string. This method is useful for ssr:
92 |
93 | ```js
94 | app._toString = (tree) => tree.toString()
95 | ```
96 |
97 | \*Required
98 |
99 | ## Plugins
100 |
101 | Some plugins to use with `monoapp` which take care of common configs:
102 |
103 | - [monoapp-react](https://github.com/jongacnik/monoapp-react)
104 | - ~~monoapp-lit-html~~ soon
105 | - ~~monoapp-nanomorph~~ soon
106 |
107 | ## More Examples
108 |
109 | - [with-react](https://github.com/jongacnik/monoapp/tree/master/examples/with-react)
110 | - [with-react-jsx](https://github.com/jongacnik/monoapp/tree/master/examples/with-react-jsx)
111 | - [with-lit-html](https://github.com/jongacnik/monoapp/tree/master/examples/with-lit-html)
112 | - [with-vue-jsx](https://github.com/jongacnik/monoapp/tree/master/examples/with-vue-jsx)
113 | - [with-nanomorph](https://github.com/jongacnik/monoapp/tree/master/examples/with-nanomorph)
114 | - [with-preact](https://github.com/jongacnik/monoapp/tree/master/examples/with-preact)
115 | - [with-inferno](https://github.com/jongacnik/monoapp/tree/master/examples/with-inferno)
116 |
117 | ## Why does this exist?
118 |
119 | `choo` is really calm and we like to build apps using it. That said, sometimes `nanohtml` and `nanomorph` aren't the best tools for the job. We wanted to be able to build apps using `choo` architecture but swap out the view layer and make use of other component ecosystems when a project calls for it.
120 |
121 | ## Notes
122 |
123 | `monoapp` is currently feature-matched to choo 6.13.1
--------------------------------------------------------------------------------