diff --git a/README.md b/README.md index ab21013..b363aaf 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![downloads][8]][9] [![js-standard-style][10]][11] A framework for creating sturdy web applications. Built on years of industry -experience and distills the essence of functional architectures into a +experience it distills the essence of functional architectures into a productive package. ## Features @@ -52,16 +52,44 @@ const tree = app.start() document.body.appendChild(tree) ``` -## Perform HTTP requests - ## Concepts - __state:__ a single object that contains all application state, should only ever be modified by `reducers` - __reducers:__ syncronous functions that modify `state` -- __effects:__ asyncronous functions that perform IO. Effects should call - `send()` when done +- __effects:__ asyncronous functions that perform IO. Effects can call + `send()` when done to handle results - __subscriptions:__ streams of data that can either be written to or read from +## Side effects +### HTTP +`choo` ships with a built-in [`http` module](https://github.com/Raynos/xhr) +that weighs only `2.4kb`: +```js +const http = require('choo/http') + +// GET JSON +http.get('/my-endpoint', { json: true }, function (err, res, body) { + if (err) throw err + if (res.statusCode !== 200 || !body) throw new Error('something went wrong') +}) + +// POST JSON +const body = { foo: 'bar' } +http.post('/my-endpoint', { json: body }, function (err, res, body) { + if (err) throw err + if (res.statusCode !== 200 || !body) throw new Error('something went wrong') +}) + +// DELETE +http.del('/my-endpoint', function (err, res) { + if (err) throw err + if (res.statusCode !== 200) throw new Error('something went wrong') +}) +``` +Note that `http` only runs in the browser to prevent accidental requests when +rendering in Node. For more details view the [`raynos/xhr` +documentation](https://github.com/Raynos/xhr). + ## API ### app = choo() Create a new `choo` app @@ -100,6 +128,7 @@ Start the application. Returns a DOM element that can be mounted using - __models:__ [`send-action`](https://github.com/sethvincent/send-action), [`xtend`](https://github.com/raynos/xtend) - __routes:__ [`sheet-router`](https://github.com/yoshuawuyts/sheet-router) +- __http:__ [`xhr`](https://github.com/Raynos/xhr) ## Optimizing To bring down file size, consider running the following `browserify` @@ -112,9 +141,10 @@ transforms: UglifyJS2. Use as a `--global` transform ## Packages that work well together -- [xhr](https://github.com/Raynos/xhr) - small XHR wrapper - [tachyons](https://github.com/tachyons-css/tachyons) - functional CSS for humans +- [sheetify](https://github.com/stackcss/sheetify) - modular CSS bundler for + browserify ## Installation ```sh diff --git a/examples/http/client.js b/examples/http/client.js new file mode 100644 index 0000000..31370a2 --- /dev/null +++ b/examples/http/client.js @@ -0,0 +1,15 @@ +const choo = require('../../') + +const mainView = require('./views/main') + +const app = choo() + +app.model(require('./models/error')) +app.model('api', require('./models/api')) + +app.router((route) => [ + route('/', mainView) +]) + +const tree = app.start() +document.body.appendChild(tree) diff --git a/examples/http/models/api.js b/examples/http/models/api.js new file mode 100644 index 0000000..37907e3 --- /dev/null +++ b/examples/http/models/api.js @@ -0,0 +1,45 @@ +const http = require('../../../http') + +module.exports = { + state: { + title: 'Button pushing machine 3000' + }, + reducers: { + set: function (action, state) { + return { 'api:title': action.payload } + } + }, + effects: { + good: performGoodRequest, + bad: performBadRequest + } +} + +function performGoodRequest (action, state, send) { + http('/good', { json: true }, function (err, res, body) { + if (err) return send('error', { payload: 'HTTP error' }) + if (res.statusCode !== 200) { + return send('error', { payload: body.payload }) + } + if (!body) { + return send('error', { payload: 'fatal: no body received' }) + } + send('api:set', { payload: body.message }) + }) +} + +function performBadRequest (action, state, send) { + http('/bad', { json: true }, function (err, res, body) { + if (err) return send('error', { payload: 'HTTP error' }) + if (res.statusCode !== 200) { + const message = (body && body.message) + ? body.message + : 'unknown server error' + return send('error', { payload: message }) + } + if (!body) { + return send('error', { payload: 'fatal: no body received' }) + } + send('api:set', { payload: body.title }) + }) +} diff --git a/examples/http/models/error.js b/examples/http/models/error.js new file mode 100644 index 0000000..4bc3694 --- /dev/null +++ b/examples/http/models/error.js @@ -0,0 +1,42 @@ +// Create a new error. Errors are pushed into an array so they can be +// queued. We're doing a bit of timestamp trickery here so each error +// is shown for exactly one second before being dropped. Probably +// useful for real world scenarios; especially when coupled to +// fancy CSS animation thingies + +// Each error is displayed 1 second +const ERROR_TIMEOUT = 1000 + +module.exports = { + state: { + error: [], + errorTimeDone: null, + triggerTime: null + }, + reducers: { + error: function (action, state) { + const now = Date.now() + const timeDone = state.errorTimeDone + const newTimestamp = (timeDone && timeDone >= now) + ? timeDone + ERROR_TIMEOUT + : now + ERROR_TIMEOUT + + return { + error: state.error.concat(action.payload), + errorTimeDone: newTimestamp + } + }, + 'error:delete': function (action, state) { + state.error.shift() + return { error: state.error } + } + }, + effects: { + error: function (action, state, send) { + const timeout = state.errorTimeDone - Date.now() + setTimeout(function () { + send('error:delete') + }, timeout) + } + } +} diff --git a/examples/http/package.json b/examples/http/package.json new file mode 100644 index 0000000..687cd91 --- /dev/null +++ b/examples/http/package.json @@ -0,0 +1,13 @@ +{ + "name": "http", + "private": true, + "main": "client.js", + "scripts": { + "start": "NODE_ENV=development node server.js" + }, + "dependencies": { + "bankai": "^2.0.2", + "browserify": "^13.0.1", + "server-router": "^2.1.0" + } +} diff --git a/examples/http/server.js b/examples/http/server.js new file mode 100644 index 0000000..c98ca55 --- /dev/null +++ b/examples/http/server.js @@ -0,0 +1,45 @@ +const serverRouter = require('server-router') +const browserify = require('browserify') +const bankai = require('bankai') +const http = require('http') + +const PORT = 8080 + +const server = http.createServer(createRouter()) +server.listen(PORT, () => console.log(`listening on port ${PORT}`)) + +var index = 0 +const errors = [ + 'a giant robot invaded robot town!', + 'something weird crawled out of the swamp', + 'oh no, the bear people invaded the server!', + 'the NSA', + '12 goats started a band', + 'we kinda just gave up' +] + +function createRouter () { + const router = serverRouter('/404') + + const html = bankai.html({ css: false }) + router.on('/', (req, res) => html(req, res).pipe(res)) + + const js = bankai.js(browserify, require.resolve('./client.js')) + router.on('/bundle.js', (req, res) => js(req, res).pipe(res)) + + router.on('/good', (req, res) => { + res.end('{ "message": "all is well in robo town!" }') + }) + + router.on('/bad', (req, res) => { + res.statusCode = 500 + res.end(`{ "message": "${errors[index++ % (errors.length)]}" }`) + }) + + router.on('/404', (req, res) => { + res.statusCode = 404 + res.end('{ "message": "the server is confused" }') + }) + + return router +} diff --git a/examples/http/views/main.js b/examples/http/views/main.js new file mode 100644 index 0000000..22c5414 --- /dev/null +++ b/examples/http/views/main.js @@ -0,0 +1,12 @@ +const choo = require('../../../') + +module.exports = function (params, state, send) { + return choo.view` +
+

${state['api:title']}

+

Latest error: ${state.error[0]}

+ + +
+ ` +} diff --git a/http.js b/http.js new file mode 100644 index 0000000..278c396 --- /dev/null +++ b/http.js @@ -0,0 +1 @@ +module.exports = require('xhr') diff --git a/package.json b/package.json index fb9fe2e..29c3f17 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "global": "^4.3.0", "send-action": "^1.1.0", "sheet-router": "^2.0.5", + "xhr": "^2.2.0", "xtend": "^4.0.1", "yo-yo": "^1.2.0" }, diff --git a/scripts/test-size b/scripts/test-size index 279de9d..0690efe 100755 --- a/scripts/test-size +++ b/scripts/test-size @@ -5,7 +5,7 @@ # -t uglifyify \ # | discify --open -browserify index.js --full-paths \ +browserify http.js --full-paths \ -g unassertify \ -g uglifyify \ | gzip-size \