diff --git a/index.js b/index.js index 05823175..ad9b59cd 100644 --- a/index.js +++ b/index.js @@ -42,7 +42,7 @@ function Choo (opts) { this._hasWindow = typeof window !== 'undefined' this._cache = opts.cache this._loaded = false - this._stores = [] + this._stores = [ondomtitlechange] this._tree = null // state @@ -66,11 +66,13 @@ function Choo (opts) { // listen for title changes; available even when calling .toString() if (this._hasWindow) this.state.title = document.title - this.emitter.prependListener(this._events.DOMTITLECHANGE, function (title) { - assert.equal(typeof title, 'string', 'events.DOMTitleChange: title should be type string') - self.state.title = title - if (self._hasWindow) document.title = title - }) + function ondomtitlechange (state) { + self.emitter.prependListener(self._events.DOMTITLECHANGE, function (title) { + assert.equal(typeof title, 'string', 'events.DOMTitleChange: title should be type string') + state.title = title + if (self._hasWindow) document.title = title + }) + } timing() } @@ -101,7 +103,7 @@ Choo.prototype.start = function () { var self = this if (this._historyEnabled) { this.emitter.prependListener(this._events.NAVIGATE, function () { - self._matchRoute() + self._matchRoute(self.state) if (self._loaded) { self.emitter.emit(self._events.RENDER) setTimeout(scrollToAnchor.bind(null, window.location.hash), 0) @@ -142,7 +144,7 @@ Choo.prototype.start = function () { } this._setCache(this.state) - this._matchRoute() + this._matchRoute(this.state) this._stores.forEach(function (initStore) { initStore(self.state) }) @@ -212,26 +214,28 @@ Choo.prototype.mount = function mount (selector) { } Choo.prototype.toString = function (location, state) { - Object.assign(this.state, state || {}) + state = state || {} + state.components = state.components || {} + state.events = Object.assign({}, state.events, this._events) 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') assert.equal(typeof location, 'string', 'choo.toString: location should be type string') - assert.equal(typeof this.state, 'object', 'choo.toString: state should be type object') + assert.equal(typeof state, 'object', 'choo.toString: state should be type object') - var self = this - this._setCache(this.state) - this._matchRoute(location) + this._setCache(state) + this._matchRoute(state, location) + this.emitter.removeAllListeners() this._stores.forEach(function (initStore) { - initStore(self.state) + initStore(state) }) - var html = this._prerender(this.state) + var html = this._prerender(state) assert.ok(html, 'choo.toString: no valid value returned for the route ' + location) assert(!Array.isArray(html), 'choo.toString: return value was an array for the route ' + location) return typeof html.outerHTML === 'string' ? html.outerHTML : html.toString() } -Choo.prototype._matchRoute = function (locationOverride) { +Choo.prototype._matchRoute = function (state, locationOverride) { var location, queryString if (locationOverride) { location = locationOverride.replace(/\?.+$/, '').replace(/\/$/, '') @@ -244,11 +248,10 @@ Choo.prototype._matchRoute = function (locationOverride) { } var matched = this.router.match(location) this._handler = matched.cb - this.state.href = location - this.state.query = nanoquery(queryString) - this.state.route = matched.route - this.state.params = matched.params - return this.state + state.href = location + state.query = nanoquery(queryString) + state.route = matched.route + state.params = matched.params } Choo.prototype._prerender = function (state) { diff --git a/test/node.js b/test/node.js index aa9471a3..8f5b345c 100644 --- a/test/node.js +++ b/test/node.js @@ -224,3 +224,37 @@ tape('state should include cache', function (t) { t.equal(arg, 'arg', 'constructor args were forwarded') } }) + +tape('state should not mutate on toString', function (t) { + t.plan(6) + + var app = choo() + app.use(store) + + var routes = ['foo', 'bar'] + var states = routes.map(function (route) { + var state = {} + app.route(`/${route}`, view) + app.toString(`/${route}`, state) + return state + }) + + for (var i = 0, len = routes.length; i < len; i++) { + t.equal(states[i].test, routes[i], 'store was used') + t.equal(states[i].title, routes[i], 'title was added to state') + } + + function store (state, emitter) { + state.test = null + emitter.on('test', function (str) { + t.equal(state.test, null, 'state has been reset') + state.test = str + }) + } + + function view (state, emit) { + emit('test', state.route) + emit(state.events.DOMTITLECHANGE, state.route) + return html`Hello ${state.route}` + } +})