├── .npmrc ├── .gitignore ├── deploy.json ├── generate-data.js ├── .travis.yml ├── src ├── render │ ├── render.js │ ├── render-header.js │ ├── render-todos.js │ ├── render-todo.js │ └── render-footer.js ├── uuid.js ├── todos.js ├── app.js └── data.json ├── webpack.config.js ├── hydrate-app.js ├── package.json ├── index.html ├── README.md └── dist ├── bottle.js ├── bottle-service.js ├── app.css └── index.html /.npmrc: -------------------------------------------------------------------------------- 1 | registry=http://registry.npmjs.org/ 2 | save-exact=true 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | .grunt/ 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /deploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "gh-pages": { 3 | "options": { 4 | "base": "dist" 5 | }, 6 | "src": [ 7 | "index.html", 8 | "*.js", 9 | "*.css" 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /generate-data.js: -------------------------------------------------------------------------------- 1 | const generateFakeTodos = require('fake-todos') 2 | 3 | const fakeTodos = generateFakeTodos(100) 4 | const outputFilename = './src/data.json' 5 | const write = require('fs').writeFileSync 6 | write(outputFilename, JSON.stringify(fakeTodos, null, 2)) 7 | 8 | console.log('wrote fake data to', outputFilename) 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | directories: 5 | - node_modules 6 | notifications: 7 | email: false 8 | node_js: 9 | - '4' 10 | before_script: 11 | - npm prune 12 | script: 13 | - npm run lint 14 | - npm test 15 | - npm run build 16 | branches: 17 | except: 18 | - "/^v\\d+\\.\\d+\\.\\d+$/" 19 | -------------------------------------------------------------------------------- /src/render/render.js: -------------------------------------------------------------------------------- 1 | const h = require('virtual-dom/h') 2 | const header = require('./render-header') 3 | const renderTodos = require('./render-todos') 4 | const footer = require('./render-footer') 5 | 6 | function render (todos) { 7 | return h('section', {className: 'todoapp'}, [ 8 | header(todos), 9 | renderTodos(todos), 10 | footer(todos) 11 | ]) 12 | } 13 | 14 | module.exports = render 15 | -------------------------------------------------------------------------------- /src/uuid.js: -------------------------------------------------------------------------------- 1 | // from http://jsfiddle.net/briguy37/2mvfd/ 2 | function uuid () { 3 | var d = new Date().getTime() 4 | var uuidFormat = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' 5 | var uuid = uuidFormat.replace(/[xy]/g, function (c) { 6 | var r = (d + Math.random() * 16) % 16 | 0 7 | d = Math.floor(d / 16) 8 | return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16) 9 | }) 10 | return uuid 11 | } 12 | 13 | module.exports = uuid 14 | -------------------------------------------------------------------------------- /src/render/render-header.js: -------------------------------------------------------------------------------- 1 | const h = require('virtual-dom/h') 2 | 3 | function render (Todos) { 4 | function isEnter (e) { 5 | return e.keyCode === 13 6 | } 7 | function onKey (e) { 8 | console.log('pressed', e.target.value) 9 | if (isEnter(e)) { 10 | Todos.add(e.target.value) 11 | e.target.value = '' 12 | } 13 | } 14 | 15 | return h('header', {className: 'header'}, [ 16 | h('h1', {}, 'todos'), 17 | h('input', { 18 | className: 'new-todo', 19 | placeholder: 'What needs to be done?', 20 | autofocus: true, 21 | onkeyup: onKey 22 | }, []) 23 | ]) 24 | } 25 | 26 | module.exports = render 27 | -------------------------------------------------------------------------------- /src/render/render-todos.js: -------------------------------------------------------------------------------- 1 | const h = require('virtual-dom/h') 2 | const renderTodo = require('./render-todo') 3 | 4 | function render (Todos) { 5 | return h('section', {className: 'main'}, [ 6 | h('input', { 7 | className: 'toggle-all', 8 | type: 'checkbox', 9 | onclick: function (e) { 10 | console.log('nothing') 11 | // Todos.mark(e.target.checked); 12 | // renderApp(); 13 | } 14 | }), 15 | h('label', {htmlFor: 'toggle-all'}, 'Mark all as complete'), 16 | h('ul', {className: 'todo-list'}, 17 | Todos.items.map(renderTodo.bind(null, Todos))) 18 | ]) 19 | } 20 | 21 | module.exports = render 22 | -------------------------------------------------------------------------------- /src/render/render-todo.js: -------------------------------------------------------------------------------- 1 | const h = require('virtual-dom/h') 2 | 3 | function render (Todos, todo) { 4 | return h('li', {className: todo.done ? 'completed' : '', key: todo.id}, [ 5 | h('div', {className: 'view'}, [ 6 | h('input', { 7 | className: 'toggle', 8 | type: 'checkbox', 9 | checked: todo.done, 10 | onchange: function (e) { 11 | Todos.mark(todo.id, e.target.checked) 12 | } 13 | }), 14 | h('label', todo.what), 15 | h('button', { 16 | className: 'destroy', 17 | onclick: function (e) { 18 | console.log('nothing') 19 | Todos.remove(todo) 20 | } 21 | }) 22 | ]) 23 | ]) 24 | } 25 | 26 | module.exports = render 27 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 2 | 3 | module.exports = { 4 | output: { 5 | path: './dist', 6 | filename: 'app.js' 7 | }, 8 | entry: { 9 | app: './src/app.js' 10 | }, 11 | module: { 12 | loaders: [ 13 | { 14 | test: /\.css$/, 15 | loader: ExtractTextPlugin.extract({ 16 | fallbackLoader: 'style-loader', 17 | loader: 'css-loader' 18 | }) 19 | }, 20 | { 21 | test: /\.json$/, 22 | loader: 'json' 23 | } 24 | ] 25 | } 26 | } 27 | 28 | module.exports.plugins = [ 29 | new ExtractTextPlugin({ 30 | filename: 'app.css', 31 | disable: false, 32 | allChunks: true 33 | }) 34 | ] 35 | -------------------------------------------------------------------------------- /src/todos.js: -------------------------------------------------------------------------------- 1 | const la = require('lazy-ass') 2 | const is = require('check-more-types') 3 | const uuid = require('./uuid') 4 | 5 | var Todos = { 6 | add: function (what) { 7 | Todos.items.unshift({ 8 | what: what, 9 | done: false, 10 | id: uuid() 11 | }) 12 | }, 13 | mark: function (id, done) { 14 | Todos.items.forEach(function (todo) { 15 | if (todo.id === id) { 16 | todo.done = done 17 | } 18 | }) 19 | }, 20 | remove: function (todo) { 21 | Todos.items = Todos.items.filter(function (t) { 22 | return t.id !== todo.id 23 | }) 24 | }, 25 | items: require('./data.json') 26 | } 27 | 28 | la(is.array(Todos.items), 'expected list of todos', Todos.items) 29 | 30 | module.exports = Todos 31 | -------------------------------------------------------------------------------- /hydrate-app.js: -------------------------------------------------------------------------------- 1 | const render = require('./src/render/render') 2 | const Todos = require('./src/todos') 3 | const rendered = render(Todos) 4 | 5 | const toHTML = require('vdom-to-html') 6 | const beautify = require('js-beautify').html 7 | const appMarkup = beautify(toHTML(rendered), { indent_size: 2 }) 8 | 9 | function updateAppMarkup (inputFilename, outputFilename, markup) { 10 | const read = require('fs').readFileSync 11 | const page = read(inputFilename, 'utf-8') 12 | 13 | const write = require('fs').writeFileSync 14 | 15 | // temp hack with ID in the closing tag 16 | const openTag = '
' 17 | const closeTag = '
' 18 | const appDiv = openTag + closeTag 19 | if (!page.indexOf(appDiv)) { 20 | throw new Error('Cannot find markup ' + appDiv + ' in the page ' + inputFilename) 21 | } 22 | const hydratedPage = page.replace(appDiv, openTag + '\n' + markup + '\n' + closeTag) 23 | 24 | write(outputFilename, hydratedPage) 25 | console.log('saved hydrated index', outputFilename) 26 | } 27 | 28 | updateAppMarkup('./index.html', './dist/index.html', appMarkup) 29 | 30 | -------------------------------------------------------------------------------- /src/render/render-footer.js: -------------------------------------------------------------------------------- 1 | const h = require('virtual-dom/h') 2 | 3 | function hashFragment () { 4 | return typeof window !== 'undefined' && window.location.hash.split('/')[1] || '' 5 | } 6 | 7 | function countRemaining (todos) { 8 | return todos.length - todos.reduce(function (count, todo) { 9 | return count + Number(todo.done) 10 | }, 0) 11 | } 12 | 13 | function hasCompleted (todos) { 14 | return todos && todos.some(function (todo) { 15 | return todo.done 16 | }) 17 | } 18 | 19 | function render (Todos) { 20 | const remaining = countRemaining(Todos.items) 21 | const route = hashFragment() 22 | 23 | return h('footer', {className: 'footer'}, [ 24 | h('span', {className: 'todo-count'}, [ 25 | h('strong', {}, String(remaining)), 26 | ' items left' 27 | ]), 28 | h('ul', {className: 'filters'}, [ 29 | h('li', [ 30 | h('a', { 31 | className: !route ? 'selected' : '', 32 | href: '#/' 33 | }, 'All') 34 | ]), 35 | h('li', [ 36 | h('a', { 37 | className: route === 'active' ? 'selected' : '', 38 | href: '#/active' 39 | }, 'Active') 40 | ]), 41 | h('li', [ 42 | h('a', { 43 | className: route === 'completed' ? 'selected' : '', 44 | href: '#/completed' 45 | }, 'Completed') 46 | ]) 47 | ]), 48 | h('button', { 49 | className: 'clear-completed', 50 | style: { 51 | display: hasCompleted(Todos.items) ? 'block' : 'none' 52 | }, 53 | onclick: function () { 54 | // todos && todos.clearCompleted(); 55 | // renderApp(); 56 | } 57 | }, 'Clear completed') 58 | ]) 59 | } 60 | 61 | module.exports = render 62 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /* global bottleService */ 4 | 5 | require('../node_modules/todomvc-common/base.css') 6 | require('../node_modules/todomvc-app-css/index.css') 7 | 8 | const la = require('lazy-ass') 9 | const is = require('check-more-types') 10 | const tinyToast = require('tiny-toast') 11 | 12 | const diff = require('virtual-dom/diff') 13 | const patch = require('virtual-dom/patch') 14 | 15 | const appNode = document.getElementById('app') 16 | var renderedNode = appNode.firstElementChild 17 | 18 | const VNode = require('virtual-dom/vnode/vnode') 19 | const VText = require('virtual-dom/vnode/vtext') 20 | const convertHTML = require('html-to-vdom')({ 21 | VNode: VNode, 22 | VText: VText 23 | }) 24 | const render = require('./render/render') 25 | var prevView = convertHTML(renderedNode.outerHTML) 26 | 27 | const Todos = require('./todos') 28 | 29 | const appLabel = 'instant-vdom-todo' 30 | const todosStorageLabel = appLabel + '-items' 31 | var updatedTodos = localStorage.getItem(todosStorageLabel) 32 | if (updatedTodos) { 33 | updatedTodos = JSON.parse(updatedTodos) 34 | Todos.items = updatedTodos 35 | console.log('set todos to local storage value with %d items', 36 | updatedTodos.length) 37 | } else { 38 | console.log('No previous todo items found') 39 | } 40 | 41 | function saveApp () { 42 | localStorage.setItem(todosStorageLabel, JSON.stringify(Todos.items)) 43 | setTimeout(function () { 44 | // application has renderd itself 45 | bottleService.refill(appLabel, 'app') 46 | }, 0); 47 | } 48 | 49 | // add rendering call after data methods 50 | // also save items 51 | Object.keys(Todos).forEach(function (key) { 52 | const value = Todos[key] 53 | if (is.fn(value)) { 54 | Todos[key] = function () { 55 | const result = value.apply(Todos, arguments) 56 | renderApp() 57 | saveApp() 58 | return result 59 | } 60 | } 61 | }) 62 | 63 | function renderApp () { 64 | console.log('rendering %d todos', Todos.items.length) 65 | const view = render(Todos) 66 | const patches = diff(prevView, view) 67 | renderedNode = patch(renderedNode, patches) 68 | prevView = view 69 | } 70 | 71 | console.log('initial render after 500ms delay') 72 | setTimeout(function () { 73 | renderApp() 74 | tinyToast.show('Rendered web app after 500ms delay') 75 | tinyToast.hide(2000) 76 | }, 500) 77 | 78 | window.renderApp = renderApp 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "instant-vdom-todo", 3 | "version": "1.0.0", 4 | "description": "Example of a build time hydration for virtual dom application", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "npm run lint", 8 | "lint": "standard --verbose --fix *.js src/**/*.js", 9 | "build": "webpack && node hydrate-app.js && npm run copy-bottle-service", 10 | "copy-bottle-service": "cp node_modules/bottle-service/dist/*.js dist/", 11 | "webpack": "webpack", 12 | "deploy": "grunty grunt-gh-pages gh-pages deploy.json", 13 | "commit": "commit-wizard", 14 | "start": "http-server dist -c-1", 15 | "dev-start": "http-server dist -c-1 -p 3005" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/bahmutov/instant-vdom-todo.git" 20 | }, 21 | "keywords": [ 22 | "virtual-dom", 23 | "vdom", 24 | "example", 25 | "hydrate", 26 | "todo" 27 | ], 28 | "private": true, 29 | "author": "Gleb Bahmutov ", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/bahmutov/instant-vdom-todo/issues" 33 | }, 34 | "homepage": "https://github.com/bahmutov/instant-vdom-todo#readme", 35 | "dependencies": { 36 | "bottle-service": "1.2.4", 37 | "check-more-types": "2.23.0", 38 | "http-server": "0.9.0", 39 | "lazy-ass": "1.5.0", 40 | "tiny-toast": "1.1.0", 41 | "todomvc-app-css": "2.0.6", 42 | "todomvc-common": "1.0.2", 43 | "vdom-to-html": "2.3.0", 44 | "virtual-dom": "2.1.1" 45 | }, 46 | "devDependencies": { 47 | "css-loader": "0.25.0", 48 | "extract-text-webpack-plugin": "2.0.0-beta.4", 49 | "fake-todos": "1.8.0", 50 | "faker": "3.1.0", 51 | "grunt": "1.0.1", 52 | "grunt-gh-pages": "2.0.0", 53 | "grunty": "0.3.0", 54 | "html-to-vdom": "0.7.0", 55 | "html2hscript": "2.0.1", 56 | "js-beautify": "1.6.4", 57 | "json-loader": "0.5.4", 58 | "pre-git": "3.10.0", 59 | "standard": "8.3.0", 60 | "style-loader": "0.13.1", 61 | "webpack": "2.1.0-beta.25" 62 | }, 63 | "config": { 64 | "pre-git": { 65 | "commit-msg": [ 66 | "simple" 67 | ], 68 | "pre-commit": [ 69 | "npm run lint", 70 | "npm run test", 71 | "npm run build" 72 | ], 73 | "pre-push": [], 74 | "post-commit": [], 75 | "post-merge": [] 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Instant TodoMVC 7 | 8 | 9 | 10 | 11 |
12 |

Prehydrated, self-rewriting TodoMVC

13 |

Uses virtual-dom, 14 | build time hydration and 15 | self-rewriting using bottle-service. 16 | Tested in Chrome desktop, see 17 | browser instructions. 18 | Read the Instant web apps blog post, 19 | or see the demo recording.

20 |
21 | 22 | 23 |
24 | 25 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # instant-vdom-todo 2 | > Prehydrated, self-rewriting TodoMVC using Virtual-Dom and bottle-service 3 | 4 | [![Build status][instant-vdom-todo-ci-image] ][instant-vdom-todo-ci-url] 5 | 6 | [live demo](https://instant-todo.herokuapp.com/), 7 | [screen recording of the demo](https://youtu.be/KRVoibtht84) - notice that 8 | I reload the page multiple times, but the app appears instantly. 9 | 10 | Uses [bottle-service](https://github.com/bahmutov/bottle-service) 11 | to enable HTML rewriting on page load. 12 | 13 | Read the [Instant web application](http://glebbahmutov.com/blog/instant-web-application/) 14 | blog post. 15 | 16 | ## Browser support 17 | 18 | ### Chrome, Opera 19 | 20 | * Nothing to do, `ServiceWorker` should be enabled by default 21 | 22 | ### Firefox 23 | 24 | * Open `about:config` 25 | * Set the `dom.serviceWorkers.enabled` setting to **true** 26 | * Set the `dom.serviceWorkers.interception.enabled` setting to **true** 27 | 28 | ### Small print 29 | 30 | Author: Gleb Bahmutov © 2015 31 | 32 | * [@bahmutov](https://twitter.com/bahmutov) 33 | * [glebbahmutov.com](http://glebbahmutov.com) 34 | * [blog](http://glebbahmutov.com/blog/) 35 | 36 | License: MIT - do anything with the code, but don't blame me if it does not work. 37 | 38 | Spread the word: tweet, star on github, etc. 39 | 40 | Support: if you find any problems with this module, email / tweet / 41 | [open issue](https://github.com/bahmutov/instant-vdom-todo/issues) on Github 42 | 43 | ## MIT License 44 | 45 | Copyright (c) 2015 Gleb Bahmutov 46 | 47 | Permission is hereby granted, free of charge, to any person 48 | obtaining a copy of this software and associated documentation 49 | files (the "Software"), to deal in the Software without 50 | restriction, including without limitation the rights to use, 51 | copy, modify, merge, publish, distribute, sublicense, and/or sell 52 | copies of the Software, and to permit persons to whom the 53 | Software is furnished to do so, subject to the following 54 | conditions: 55 | 56 | The above copyright notice and this permission notice shall be 57 | included in all copies or substantial portions of the Software. 58 | 59 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 60 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 61 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 62 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 63 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 64 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 65 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 66 | OTHER DEALINGS IN THE SOFTWARE. 67 | 68 | [instant-vdom-todo-ci-image]: https://travis-ci.org/bahmutov/instant-vdom-todo.svg?branch=master 69 | [instant-vdom-todo-ci-url]: https://travis-ci.org/bahmutov/instant-vdom-todo 70 | -------------------------------------------------------------------------------- /dist/bottle.js: -------------------------------------------------------------------------------- 1 | !(function startBottleService (root) { 2 | 'use strict' 3 | 4 | if (!root.navigator) { 5 | console.error('Missing navigator') 6 | return 7 | } 8 | 9 | if (!root.navigator.serviceWorker) { 10 | console.error('Sorry, not ServiceWorker feature, maybe enable it?') 11 | console.error('http://jakearchibald.com/2014/using-serviceworker-today/') 12 | return 13 | } 14 | 15 | // TODO package lazy-ass and check-more-types using webpack 16 | 17 | function toString (x) { 18 | return typeof x === 'string' ? x : JSON.stringify(x) 19 | } 20 | 21 | function la (condition) { 22 | if (!condition) { 23 | var args = Array.prototype.slice.call(arguments, 1) 24 | .map(toString) 25 | throw new Error(args.join(' ')) 26 | } 27 | } 28 | 29 | function isFunction (f) { 30 | return typeof f === 'function' 31 | } 32 | 33 | function getCurrentScriptFolder () { 34 | var scriptEls = document.getElementsByTagName('script') 35 | var thisScriptEl = scriptEls[scriptEls.length - 1] 36 | var scriptPath = thisScriptEl.src 37 | return scriptPath.substr(0, scriptPath.lastIndexOf('/') + 1) 38 | } 39 | 40 | var serviceScriptUrl = getCurrentScriptFolder() + 'bottle-service.js' 41 | // assume we are running at /pathname 42 | var scope = window.location.pathname 43 | 44 | var send = function mockSend () { 45 | console.error('Bottle service not initialized yet') 46 | } 47 | 48 | function registeredWorker (registration) { 49 | la(registration, 'missing service worker registration') 50 | la(registration.active, 'missing active service worker') 51 | la(isFunction(registration.active.postMessage), 52 | 'expected function postMessage to communicate with service worker') 53 | 54 | send = registration.active.postMessage.bind(registration.active) 55 | var info = '\nbottle-service - .\n' + 56 | 'I have a valid service-turtle, use `bottleService` object to update cached page' 57 | console.log(info) 58 | 59 | registration.active.onmessage = function messageFromServiceWorker (e) { 60 | console.log('received message from the service worker', e) 61 | } 62 | } 63 | 64 | function onError (err) { 65 | if (err.message.indexOf('missing active') !== -1) { 66 | // the service worker is installed 67 | window.location.reload() 68 | } else { 69 | console.error('bottle service error', err) 70 | } 71 | } 72 | 73 | root.navigator.serviceWorker.register(serviceScriptUrl, { scope: scope }) 74 | .then(registeredWorker) 75 | .catch(onError) 76 | 77 | root.bottleService = { 78 | refill: function refill (applicationName, id) { 79 | console.log('bottle-service: html for app %s element %s', applicationName, id) 80 | 81 | var el = document.getElementById(id) 82 | la(el, 'could not find element with id', id) 83 | var html = el.innerHTML.trim() 84 | send({ 85 | cmd: 'refill', 86 | html: html, 87 | name: applicationName, 88 | id: id 89 | }) 90 | }, 91 | print: function print (applicationName) { 92 | send({ 93 | cmd: 'print', 94 | name: applicationName 95 | }) 96 | }, 97 | clear: function clear (applicationName) { 98 | send({ 99 | cmd: 'clear', 100 | name: applicationName 101 | }) 102 | } 103 | } 104 | }(window)) 105 | -------------------------------------------------------------------------------- /dist/bottle-service.js: -------------------------------------------------------------------------------- 1 | /******/ (function(modules) { // webpackBootstrap 2 | /******/ // The module cache 3 | /******/ var installedModules = {}; 4 | 5 | /******/ // The require function 6 | /******/ function __webpack_require__(moduleId) { 7 | 8 | /******/ // Check if module is in cache 9 | /******/ if(installedModules[moduleId]) 10 | /******/ return installedModules[moduleId].exports; 11 | 12 | /******/ // Create a new module (and put it into the cache) 13 | /******/ var module = installedModules[moduleId] = { 14 | /******/ exports: {}, 15 | /******/ id: moduleId, 16 | /******/ loaded: false 17 | /******/ }; 18 | 19 | /******/ // Execute the module function 20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 21 | 22 | /******/ // Flag the module as loaded 23 | /******/ module.loaded = true; 24 | 25 | /******/ // Return the exports of the module 26 | /******/ return module.exports; 27 | /******/ } 28 | 29 | 30 | /******/ // expose the modules object (__webpack_modules__) 31 | /******/ __webpack_require__.m = modules; 32 | 33 | /******/ // expose the module cache 34 | /******/ __webpack_require__.c = installedModules; 35 | 36 | /******/ // __webpack_public_path__ 37 | /******/ __webpack_require__.p = ""; 38 | 39 | /******/ // Load entry module and return exports 40 | /******/ return __webpack_require__(0); 41 | /******/ }) 42 | /************************************************************************/ 43 | /******/ ([ 44 | /* 0 */ 45 | /***/ function(module, exports, __webpack_require__) { 46 | 47 | 'use strict' 48 | 49 | /* 50 | This is ServiceWorker code 51 | */ 52 | /* global self, Response, Promise, location, fetch */ 53 | var myName = 'bottle-service' 54 | console.log(myName, 'startup') 55 | 56 | function dataStore () { 57 | var cachesStorage = __webpack_require__(1) 58 | return cachesStorage(myName) 59 | } 60 | 61 | self.addEventListener('install', function (event) { 62 | console.log(myName, 'installed') 63 | }) 64 | 65 | self.addEventListener('activate', function () { 66 | console.log(myName, 'activated') 67 | }) 68 | 69 | var baseHref = location.href.substr(0, location.href.indexOf('bottle-service.js')) 70 | function isIndexPageRequest (event) { 71 | return event && 72 | event.request && 73 | event.request.url === baseHref 74 | } 75 | 76 | self.addEventListener('fetch', function (event) { 77 | if (!isIndexPageRequest(event)) { 78 | return fetch(event.request) 79 | } 80 | 81 | console.log(myName, 'fetching index page', event.request.url) 82 | 83 | event.respondWith( 84 | fetch(event.request) 85 | .then(function (response) { 86 | return dataStore() 87 | .then(function (store) { 88 | return store.getItem('contents') 89 | }) 90 | .then(function (contents) { 91 | if (contents && contents.html && contents.id) { 92 | console.log('fetched latest', response.url, 'need to update') 93 | console.log('element "%s" with html "%s" ...', 94 | contents.id, contents.html.substr(0, 15)) 95 | 96 | var copy = response.clone() 97 | return copy.text().then(function (pageHtml) { 98 | console.log('inserting our html') 99 | // HACK using id in the CLOSING TAG to find fragment 100 | var toReplaceStart = '
' 101 | var toReplaceFinish = '
' 102 | var startIndex = pageHtml.indexOf(toReplaceStart) 103 | var finishIndex = pageHtml.indexOf(toReplaceFinish) 104 | if (startIndex !== -1 && finishIndex > startIndex) { 105 | console.log('found fragment') 106 | pageHtml = pageHtml.substr(0, startIndex + toReplaceStart.length) + 107 | '\n' + contents.html + '\n' + 108 | pageHtml.substr(finishIndex) 109 | } 110 | 111 | // console.log('page html') 112 | // console.log(pageHtml) 113 | 114 | var responseOptions = { 115 | status: 200, 116 | headers: { 117 | 'Content-Type': 'text/html charset=UTF-8' 118 | } 119 | } 120 | return new Response(pageHtml, responseOptions) 121 | }) 122 | } else { 123 | return response 124 | } 125 | }, function notFound () { 126 | return response 127 | }) 128 | }) 129 | ) 130 | }) 131 | 132 | // use window.navigator.serviceWorker.controller.postMessage('hi') 133 | // to communicate with this service worker 134 | self.onmessage = function onMessage (event) { 135 | console.log('message to bottle-service worker cmd', event.data && event.data.cmd) 136 | 137 | // TODO how to use application name? 138 | 139 | dataStore().then(function (store) { 140 | switch (event.data.cmd) { 141 | case 'print': { 142 | return store.getItem('contents') 143 | .then(function (res) { 144 | console.log('bottle service has contents') 145 | console.log(res) 146 | }) 147 | } 148 | case 'clear': { 149 | console.log('clearing the bottle') 150 | return store.setItem('contents', {}) 151 | } 152 | case 'refill': { 153 | return store.setItem('contents', { 154 | html: event.data.html, 155 | id: event.data.id 156 | }).then(function () { 157 | console.log('saved new html for id', event.data.id) 158 | }) 159 | } 160 | default: { 161 | console.error(myName, 'unknown command', event.data) 162 | } 163 | } 164 | }) 165 | } 166 | 167 | 168 | /***/ }, 169 | /* 1 */ 170 | /***/ function(module, exports) { 171 | 172 | // Poor man's async "localStorage" on top of Cache 173 | // https://developer.mozilla.org/en-US/docs/Web/API/Cache 174 | if (typeof caches === 'undefined') { 175 | throw new Error('Cannot find object caches?! Cannot init cache-storage') 176 | } 177 | /* global caches, Response */ 178 | function dataStore (name) { 179 | var id = name ? name + '-v1' : 'cache-storage-v1' 180 | return caches.open(id) 181 | .then(function (cache) { 182 | return { 183 | setItem: function (key, data) { 184 | return cache.put(key, new Response(JSON.stringify(data))) 185 | }, 186 | getItem: function (key) { 187 | return cache.match(key) 188 | .then(function (res) { 189 | return res && 190 | res.text().then(JSON.parse) 191 | }) 192 | } 193 | } 194 | }) 195 | } 196 | 197 | module.exports = dataStore 198 | 199 | 200 | /***/ } 201 | /******/ ]); -------------------------------------------------------------------------------- /dist/app.css: -------------------------------------------------------------------------------- 1 | hr { 2 | margin: 20px 0; 3 | border: 0; 4 | border-top: 1px dashed #c5c5c5; 5 | border-bottom: 1px dashed #f7f7f7; 6 | } 7 | 8 | .learn a { 9 | font-weight: normal; 10 | text-decoration: none; 11 | color: #b83f45; 12 | } 13 | 14 | .learn a:hover { 15 | text-decoration: underline; 16 | color: #787e7e; 17 | } 18 | 19 | .learn h3, 20 | .learn h4, 21 | .learn h5 { 22 | margin: 10px 0; 23 | font-weight: 500; 24 | line-height: 1.2; 25 | color: #000; 26 | } 27 | 28 | .learn h3 { 29 | font-size: 24px; 30 | } 31 | 32 | .learn h4 { 33 | font-size: 18px; 34 | } 35 | 36 | .learn h5 { 37 | margin-bottom: 0; 38 | font-size: 14px; 39 | } 40 | 41 | .learn ul { 42 | padding: 0; 43 | margin: 0 0 30px 25px; 44 | } 45 | 46 | .learn li { 47 | line-height: 20px; 48 | } 49 | 50 | .learn p { 51 | font-size: 15px; 52 | font-weight: 300; 53 | line-height: 1.3; 54 | margin-top: 0; 55 | margin-bottom: 0; 56 | } 57 | 58 | #issue-count { 59 | display: none; 60 | } 61 | 62 | .quote { 63 | border: none; 64 | margin: 20px 0 60px 0; 65 | } 66 | 67 | .quote p { 68 | font-style: italic; 69 | } 70 | 71 | .quote p:before { 72 | content: '\201C'; 73 | font-size: 50px; 74 | opacity: .15; 75 | position: absolute; 76 | top: -20px; 77 | left: 3px; 78 | } 79 | 80 | .quote p:after { 81 | content: '\201D'; 82 | font-size: 50px; 83 | opacity: .15; 84 | position: absolute; 85 | bottom: -42px; 86 | right: 3px; 87 | } 88 | 89 | .quote footer { 90 | position: absolute; 91 | bottom: -40px; 92 | right: 0; 93 | } 94 | 95 | .quote footer img { 96 | border-radius: 3px; 97 | } 98 | 99 | .quote footer a { 100 | margin-left: 5px; 101 | vertical-align: middle; 102 | } 103 | 104 | .speech-bubble { 105 | position: relative; 106 | padding: 10px; 107 | background: rgba(0, 0, 0, .04); 108 | border-radius: 5px; 109 | } 110 | 111 | .speech-bubble:after { 112 | content: ''; 113 | position: absolute; 114 | top: 100%; 115 | right: 30px; 116 | border: 13px solid transparent; 117 | border-top-color: rgba(0, 0, 0, .04); 118 | } 119 | 120 | .learn-bar > .learn { 121 | position: absolute; 122 | width: 272px; 123 | top: 8px; 124 | left: -300px; 125 | padding: 10px; 126 | border-radius: 5px; 127 | background-color: rgba(255, 255, 255, .6); 128 | transition-property: left; 129 | transition-duration: 500ms; 130 | } 131 | 132 | @media (min-width: 899px) { 133 | .learn-bar { 134 | width: auto; 135 | padding-left: 300px; 136 | } 137 | 138 | .learn-bar > .learn { 139 | left: 8px; 140 | } 141 | } 142 | html, 143 | body { 144 | margin: 0; 145 | padding: 0; 146 | } 147 | 148 | button { 149 | margin: 0; 150 | padding: 0; 151 | border: 0; 152 | background: none; 153 | font-size: 100%; 154 | vertical-align: baseline; 155 | font-family: inherit; 156 | font-weight: inherit; 157 | color: inherit; 158 | -webkit-appearance: none; 159 | appearance: none; 160 | -webkit-font-smoothing: antialiased; 161 | -moz-osx-font-smoothing: grayscale; 162 | } 163 | 164 | body { 165 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 166 | line-height: 1.4em; 167 | background: #f5f5f5; 168 | color: #4d4d4d; 169 | min-width: 230px; 170 | max-width: 550px; 171 | margin: 0 auto; 172 | -webkit-font-smoothing: antialiased; 173 | -moz-osx-font-smoothing: grayscale; 174 | font-weight: 300; 175 | } 176 | 177 | :focus { 178 | outline: 0; 179 | } 180 | 181 | .hidden { 182 | display: none; 183 | } 184 | 185 | .todoapp { 186 | background: #fff; 187 | margin: 130px 0 40px 0; 188 | position: relative; 189 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 190 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 191 | } 192 | 193 | .todoapp input::-webkit-input-placeholder { 194 | font-style: italic; 195 | font-weight: 300; 196 | color: #e6e6e6; 197 | } 198 | 199 | .todoapp input::-moz-placeholder { 200 | font-style: italic; 201 | font-weight: 300; 202 | color: #e6e6e6; 203 | } 204 | 205 | .todoapp input::input-placeholder { 206 | font-style: italic; 207 | font-weight: 300; 208 | color: #e6e6e6; 209 | } 210 | 211 | .todoapp h1 { 212 | position: absolute; 213 | top: -155px; 214 | width: 100%; 215 | font-size: 100px; 216 | font-weight: 100; 217 | text-align: center; 218 | color: rgba(175, 47, 47, 0.15); 219 | -webkit-text-rendering: optimizeLegibility; 220 | -moz-text-rendering: optimizeLegibility; 221 | text-rendering: optimizeLegibility; 222 | } 223 | 224 | .new-todo, 225 | .edit { 226 | position: relative; 227 | margin: 0; 228 | width: 100%; 229 | font-size: 24px; 230 | font-family: inherit; 231 | font-weight: inherit; 232 | line-height: 1.4em; 233 | border: 0; 234 | color: inherit; 235 | padding: 6px; 236 | border: 1px solid #999; 237 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 238 | box-sizing: border-box; 239 | -webkit-font-smoothing: antialiased; 240 | -moz-osx-font-smoothing: grayscale; 241 | } 242 | 243 | .new-todo { 244 | padding: 16px 16px 16px 60px; 245 | border: none; 246 | background: rgba(0, 0, 0, 0.003); 247 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); 248 | } 249 | 250 | .main { 251 | position: relative; 252 | z-index: 2; 253 | border-top: 1px solid #e6e6e6; 254 | } 255 | 256 | label[for='toggle-all'] { 257 | display: none; 258 | } 259 | 260 | .toggle-all { 261 | position: absolute; 262 | top: -55px; 263 | left: -12px; 264 | width: 60px; 265 | height: 34px; 266 | text-align: center; 267 | border: none; /* Mobile Safari */ 268 | } 269 | 270 | .toggle-all:before { 271 | content: '\276F'; 272 | font-size: 22px; 273 | color: #e6e6e6; 274 | padding: 10px 27px 10px 27px; 275 | } 276 | 277 | .toggle-all:checked:before { 278 | color: #737373; 279 | } 280 | 281 | .todo-list { 282 | margin: 0; 283 | padding: 0; 284 | list-style: none; 285 | } 286 | 287 | .todo-list li { 288 | position: relative; 289 | font-size: 24px; 290 | border-bottom: 1px solid #ededed; 291 | } 292 | 293 | .todo-list li:last-child { 294 | border-bottom: none; 295 | } 296 | 297 | .todo-list li.editing { 298 | border-bottom: none; 299 | padding: 0; 300 | } 301 | 302 | .todo-list li.editing .edit { 303 | display: block; 304 | width: 506px; 305 | padding: 12px 16px; 306 | margin: 0 0 0 43px; 307 | } 308 | 309 | .todo-list li.editing .view { 310 | display: none; 311 | } 312 | 313 | .todo-list li .toggle { 314 | text-align: center; 315 | width: 40px; 316 | /* auto, since non-WebKit browsers doesn't support input styling */ 317 | height: auto; 318 | position: absolute; 319 | top: 0; 320 | bottom: 0; 321 | margin: auto 0; 322 | border: none; /* Mobile Safari */ 323 | -webkit-appearance: none; 324 | appearance: none; 325 | } 326 | 327 | .todo-list li .toggle:after { 328 | content: url('data:image/svg+xml;utf8,'); 329 | } 330 | 331 | .todo-list li .toggle:checked:after { 332 | content: url('data:image/svg+xml;utf8,'); 333 | } 334 | 335 | .todo-list li label { 336 | word-break: break-all; 337 | padding: 15px 60px 15px 15px; 338 | margin-left: 45px; 339 | display: block; 340 | line-height: 1.2; 341 | transition: color 0.4s; 342 | } 343 | 344 | .todo-list li.completed label { 345 | color: #d9d9d9; 346 | text-decoration: line-through; 347 | } 348 | 349 | .todo-list li .destroy { 350 | display: none; 351 | position: absolute; 352 | top: 0; 353 | right: 10px; 354 | bottom: 0; 355 | width: 40px; 356 | height: 40px; 357 | margin: auto 0; 358 | font-size: 30px; 359 | color: #cc9a9a; 360 | margin-bottom: 11px; 361 | transition: color 0.2s ease-out; 362 | } 363 | 364 | .todo-list li .destroy:hover { 365 | color: #af5b5e; 366 | } 367 | 368 | .todo-list li .destroy:after { 369 | content: '\D7'; 370 | } 371 | 372 | .todo-list li:hover .destroy { 373 | display: block; 374 | } 375 | 376 | .todo-list li .edit { 377 | display: none; 378 | } 379 | 380 | .todo-list li.editing:last-child { 381 | margin-bottom: -1px; 382 | } 383 | 384 | .footer { 385 | color: #777; 386 | padding: 10px 15px; 387 | height: 20px; 388 | text-align: center; 389 | border-top: 1px solid #e6e6e6; 390 | } 391 | 392 | .footer:before { 393 | content: ''; 394 | position: absolute; 395 | right: 0; 396 | bottom: 0; 397 | left: 0; 398 | height: 50px; 399 | overflow: hidden; 400 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 401 | 0 8px 0 -3px #f6f6f6, 402 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 403 | 0 16px 0 -6px #f6f6f6, 404 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 405 | } 406 | 407 | .todo-count { 408 | float: left; 409 | text-align: left; 410 | } 411 | 412 | .todo-count strong { 413 | font-weight: 300; 414 | } 415 | 416 | .filters { 417 | margin: 0; 418 | padding: 0; 419 | list-style: none; 420 | position: absolute; 421 | right: 0; 422 | left: 0; 423 | } 424 | 425 | .filters li { 426 | display: inline; 427 | } 428 | 429 | .filters li a { 430 | color: inherit; 431 | margin: 3px; 432 | padding: 3px 7px; 433 | text-decoration: none; 434 | border: 1px solid transparent; 435 | border-radius: 3px; 436 | } 437 | 438 | .filters li a:hover { 439 | border-color: rgba(175, 47, 47, 0.1); 440 | } 441 | 442 | .filters li a.selected { 443 | border-color: rgba(175, 47, 47, 0.2); 444 | } 445 | 446 | .clear-completed, 447 | html .clear-completed:active { 448 | float: right; 449 | position: relative; 450 | line-height: 20px; 451 | text-decoration: none; 452 | cursor: pointer; 453 | } 454 | 455 | .clear-completed:hover { 456 | text-decoration: underline; 457 | } 458 | 459 | .info { 460 | margin: 65px auto 0; 461 | color: #bfbfbf; 462 | font-size: 10px; 463 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 464 | text-align: center; 465 | } 466 | 467 | .info p { 468 | line-height: 1; 469 | } 470 | 471 | .info a { 472 | color: inherit; 473 | text-decoration: none; 474 | font-weight: 400; 475 | } 476 | 477 | .info a:hover { 478 | text-decoration: underline; 479 | } 480 | 481 | /* 482 | Hack to remove background from Mobile Safari. 483 | Can't use it globally since it destroys checkboxes in Firefox 484 | */ 485 | @media screen and (-webkit-min-device-pixel-ratio:0) { 486 | .toggle-all, 487 | .todo-list li .toggle { 488 | background: none; 489 | } 490 | 491 | .todo-list li .toggle { 492 | height: 40px; 493 | } 494 | 495 | .toggle-all { 496 | -webkit-transform: rotate(90deg); 497 | transform: rotate(90deg); 498 | -webkit-appearance: none; 499 | appearance: none; 500 | } 501 | } 502 | 503 | @media (max-width: 430px) { 504 | .footer { 505 | height: 50px; 506 | } 507 | 508 | .filters { 509 | bottom: 10px; 510 | } 511 | } 512 | -------------------------------------------------------------------------------- /src/data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "what": "help fix fines", 4 | "due": "tomorrow", 5 | "done": false, 6 | "id": "db2f5b89-7143-4be1-a25d-aafda9b95f31" 7 | }, 8 | { 9 | "what": "skip exercising fines", 10 | "due": "tomorrow", 11 | "done": false, 12 | "id": "54cb20a5-56d3-4c82-bb27-e8944981aee9" 13 | }, 14 | { 15 | "what": "sell books", 16 | "due": "tomorrow", 17 | "done": false, 18 | "id": "ea373b4d-24cf-457a-9ca3-8ab10f3cbb84" 19 | }, 20 | { 21 | "what": "pretend to pick principles", 22 | "due": "tomorrow", 23 | "done": false, 24 | "id": "69e5161e-d038-42aa-b5cb-9a6b3f5fa529" 25 | }, 26 | { 27 | "what": "buy bird", 28 | "due": "tomorrow", 29 | "done": false, 30 | "id": "1d1ec006-d05b-4d4a-a82c-9f1f88d332e3" 31 | }, 32 | { 33 | "what": "tweet Node.js", 34 | "due": "tomorrow", 35 | "done": false, 36 | "id": "d2db157a-d13b-41ac-9379-12ea5862ef17" 37 | }, 38 | { 39 | "what": "help buy castle", 40 | "due": "tomorrow", 41 | "done": false, 42 | "id": "4db10c6f-acb5-4695-9255-5d2f5ecf39ed" 43 | }, 44 | { 45 | "what": "help avoid fishing rod", 46 | "due": "tomorrow", 47 | "done": false, 48 | "id": "687c5467-8ec9-45a6-a4b0-88a5caea195c" 49 | }, 50 | { 51 | "what": "promote principles", 52 | "due": "tomorrow", 53 | "done": false, 54 | "id": "9119ec04-748c-4a23-95b6-352a6e2379ba" 55 | }, 56 | { 57 | "what": "try to tweet distant relatives", 58 | "due": "tomorrow", 59 | "done": false, 60 | "id": "ca5fc106-89e3-4a75-8ef6-2ce67fde5f2f" 61 | }, 62 | { 63 | "what": "pick bird", 64 | "due": "tomorrow", 65 | "done": false, 66 | "id": "55149f97-d2e9-4bf6-a3f2-115880c50c76" 67 | }, 68 | { 69 | "what": "add bird", 70 | "due": "tomorrow", 71 | "done": false, 72 | "id": "97a31a80-cf42-4970-9cc3-b1f67da623a3" 73 | }, 74 | { 75 | "what": "skip exercising needle work", 76 | "due": "tomorrow", 77 | "done": false, 78 | "id": "860423a6-8fc4-4129-8059-97102f9f2132" 79 | }, 80 | { 81 | "what": "buy distant relatives", 82 | "due": "tomorrow", 83 | "done": false, 84 | "id": "7e39613a-fd6a-49b2-900b-cc63151be19f" 85 | }, 86 | { 87 | "what": "promote Italian", 88 | "due": "tomorrow", 89 | "done": false, 90 | "id": "111b7758-1dfc-414f-bf22-0fc138312089" 91 | }, 92 | { 93 | "what": "try to code laptop", 94 | "due": "tomorrow", 95 | "done": false, 96 | "id": "36f8afa1-831b-4361-bd8f-0ca9569c1c94" 97 | }, 98 | { 99 | "what": "fix chess", 100 | "due": "tomorrow", 101 | "done": false, 102 | "id": "4b77d626-40ba-43f6-80dc-ddf5aca037b6" 103 | }, 104 | { 105 | "what": "try to code bird", 106 | "due": "tomorrow", 107 | "done": false, 108 | "id": "411dae7a-35e4-4a01-8a0f-46a15fec9445" 109 | }, 110 | { 111 | "what": "make distant relatives", 112 | "due": "tomorrow", 113 | "done": false, 114 | "id": "30364285-b2ad-4b57-aade-3bfe2e5f45a5" 115 | }, 116 | { 117 | "what": "pretend to buy fines", 118 | "due": "tomorrow", 119 | "done": false, 120 | "id": "3d9cf7df-73ca-4fa9-8ca2-b1c992ad9a1e" 121 | }, 122 | { 123 | "what": "make Node.js", 124 | "due": "tomorrow", 125 | "done": false, 126 | "id": "f9b60833-ff75-41c4-a38e-1159f37ee687" 127 | }, 128 | { 129 | "what": "make castle", 130 | "due": "tomorrow", 131 | "done": false, 132 | "id": "78afe710-2473-4f4d-89c2-1b230d84c364" 133 | }, 134 | { 135 | "what": "avoid crashing boots", 136 | "due": "tomorrow", 137 | "done": false, 138 | "id": "12e16af5-498b-41a3-ad99-d27f271ee732" 139 | }, 140 | { 141 | "what": "submit books", 142 | "due": "tomorrow", 143 | "done": false, 144 | "id": "b4280a63-5c90-4ddc-83d4-4c11a3e36c5b" 145 | }, 146 | { 147 | "what": "skip avoiding milk", 148 | "due": "tomorrow", 149 | "done": false, 150 | "id": "d62cb8f3-a5d4-4851-a578-90672ba7f541" 151 | }, 152 | { 153 | "what": "pretend to clean distant relatives", 154 | "due": "tomorrow", 155 | "done": false, 156 | "id": "368f499c-69fc-42a0-97ac-f2920d5b222b" 157 | }, 158 | { 159 | "what": "make fines", 160 | "due": "tomorrow", 161 | "done": false, 162 | "id": "7dd2f859-3e98-4b9d-9b75-d204fa111d2e" 163 | }, 164 | { 165 | "what": "skip throwing milk", 166 | "due": "tomorrow", 167 | "done": false, 168 | "id": "0f790690-a29d-416e-8a70-bd498f5697c5" 169 | }, 170 | { 171 | "what": "promote boots", 172 | "due": "tomorrow", 173 | "done": false, 174 | "id": "bca35e5f-c758-4c0f-8b08-bdf0b4bd2524" 175 | }, 176 | { 177 | "what": "play knife", 178 | "due": "tomorrow", 179 | "done": false, 180 | "id": "a02292d9-74a0-4a13-a798-79bc55e2153f" 181 | }, 182 | { 183 | "what": "try to avoid adults", 184 | "due": "tomorrow", 185 | "done": false, 186 | "id": "3ca10aa8-3f90-4b9a-bf5b-7959b108a4c6" 187 | }, 188 | { 189 | "what": "tweet fines", 190 | "due": "tomorrow", 191 | "done": false, 192 | "id": "cc07b3e2-f734-4f6a-8317-1d314d27d816" 193 | }, 194 | { 195 | "what": "avoid chess", 196 | "due": "tomorrow", 197 | "done": false, 198 | "id": "50f66997-e070-45d8-b388-8dc4a4c7d739" 199 | }, 200 | { 201 | "what": "forget adults", 202 | "due": "tomorrow", 203 | "done": false, 204 | "id": "2d8dac09-c602-4caa-b13a-21fb4bf5a8bd" 205 | }, 206 | { 207 | "what": "pick distant relatives", 208 | "due": "tomorrow", 209 | "done": false, 210 | "id": "3fa0f111-0b93-4f38-821b-1b6bcf7e9e8a" 211 | }, 212 | { 213 | "what": "help code knife", 214 | "due": "tomorrow", 215 | "done": false, 216 | "id": "1f93ce72-20c4-454e-bac1-ae44124d321d" 217 | }, 218 | { 219 | "what": "crash distant relatives", 220 | "due": "tomorrow", 221 | "done": false, 222 | "id": "d636e89a-a5a1-405d-8b64-0898eff6f489" 223 | }, 224 | { 225 | "what": "try to forget principles", 226 | "due": "tomorrow", 227 | "done": false, 228 | "id": "ae761cb2-6e1d-4188-be9e-5a375e54097e" 229 | }, 230 | { 231 | "what": "promote Italian", 232 | "due": "tomorrow", 233 | "done": false, 234 | "id": "c9a36858-acd4-4273-9554-057b7cc75c8c" 235 | }, 236 | { 237 | "what": "avoid making distant relatives", 238 | "due": "tomorrow", 239 | "done": false, 240 | "id": "bac8d783-5c57-4819-a27c-b823991ee188" 241 | }, 242 | { 243 | "what": "pretend to exercise charges", 244 | "due": "tomorrow", 245 | "done": false, 246 | "id": "8408dcc7-a8d8-4a9b-b7a4-c1b85622c50a" 247 | }, 248 | { 249 | "what": "help do castle", 250 | "due": "tomorrow", 251 | "done": false, 252 | "id": "0260a1b5-84e4-4ad5-8a1f-fda53eace79c" 253 | }, 254 | { 255 | "what": "tweet Node.js", 256 | "due": "tomorrow", 257 | "done": false, 258 | "id": "83872342-eaf0-4c5e-b55a-bd2fcafa927a" 259 | }, 260 | { 261 | "what": "pretend to find fines", 262 | "due": "tomorrow", 263 | "done": false, 264 | "id": "76227a1c-ba09-4b7f-8800-0ba652ffeb07" 265 | }, 266 | { 267 | "what": "make Node.js", 268 | "due": "tomorrow", 269 | "done": false, 270 | "id": "8424e9f6-675a-459d-b044-74085a43c223" 271 | }, 272 | { 273 | "what": "pretend to buy fishing rod", 274 | "due": "tomorrow", 275 | "done": false, 276 | "id": "785f758c-44e3-445a-897d-fd6b865b40a7" 277 | }, 278 | { 279 | "what": "avoid milk", 280 | "due": "tomorrow", 281 | "done": false, 282 | "id": "f91d84a6-d555-42a1-b51d-aca1a200e939" 283 | }, 284 | { 285 | "what": "fix chess", 286 | "due": "tomorrow", 287 | "done": false, 288 | "id": "830325d9-cd54-4031-806c-c431eecc6fe6" 289 | }, 290 | { 291 | "what": "pretend to buy Node.js", 292 | "due": "tomorrow", 293 | "done": false, 294 | "id": "3d74ca36-8b11-471d-9cb0-5c75de529095" 295 | }, 296 | { 297 | "what": "try to tweet milk", 298 | "due": "tomorrow", 299 | "done": false, 300 | "id": "05eca89c-9c07-419a-9849-d0bb877448eb" 301 | }, 302 | { 303 | "what": "help promote principles", 304 | "due": "tomorrow", 305 | "done": false, 306 | "id": "1c5d10eb-c792-4106-8be2-d89dda1aa75d" 307 | }, 308 | { 309 | "what": "try to promote fines", 310 | "due": "tomorrow", 311 | "done": false, 312 | "id": "ae76add3-4ee6-48e1-bdf4-a9cb336b5bfe" 313 | }, 314 | { 315 | "what": "try to play adults", 316 | "due": "tomorrow", 317 | "done": false, 318 | "id": "972bc87c-0437-4d27-aa6a-2d808268e717" 319 | }, 320 | { 321 | "what": "fix adults", 322 | "due": "tomorrow", 323 | "done": false, 324 | "id": "270bdc2c-781f-4f71-bb91-83fc11426623" 325 | }, 326 | { 327 | "what": "promote boots", 328 | "due": "tomorrow", 329 | "done": false, 330 | "id": "be153dff-fa1f-463e-83ec-48daf1391e70" 331 | }, 332 | { 333 | "what": "submit knife", 334 | "due": "tomorrow", 335 | "done": false, 336 | "id": "c40a01ba-4bbe-497e-a058-b444dfc1f012" 337 | }, 338 | { 339 | "what": "fix needle work", 340 | "due": "tomorrow", 341 | "done": false, 342 | "id": "5b800671-644c-4616-93f1-ab6f75502f9f" 343 | }, 344 | { 345 | "what": "make laptop", 346 | "due": "tomorrow", 347 | "done": false, 348 | "id": "6d3360bd-07d1-4db1-b8f8-aa4399ca9c28" 349 | }, 350 | { 351 | "what": "play charges", 352 | "due": "tomorrow", 353 | "done": false, 354 | "id": "6fd5180a-f354-4d86-9036-4ae459bffacf" 355 | }, 356 | { 357 | "what": "play Italian", 358 | "due": "tomorrow", 359 | "done": false, 360 | "id": "1faa6017-cbd8-4ba9-a5d3-47e5bd40b50d" 361 | }, 362 | { 363 | "what": "skip cleaning bird", 364 | "due": "tomorrow", 365 | "done": false, 366 | "id": "50f58690-a02d-4936-b073-a6feb814c950" 367 | }, 368 | { 369 | "what": "pick boots", 370 | "due": "tomorrow", 371 | "done": false, 372 | "id": "038c1d0f-59cb-463c-8764-801670f6d556" 373 | }, 374 | { 375 | "what": "pretend to skip bird", 376 | "due": "tomorrow", 377 | "done": false, 378 | "id": "8fbf1e0c-a195-40e9-b0ee-0d67d5ed3a12" 379 | }, 380 | { 381 | "what": "pretend to throw boots", 382 | "due": "tomorrow", 383 | "done": false, 384 | "id": "e56b4fcc-65e2-497d-af8d-59727d7f525b" 385 | }, 386 | { 387 | "what": "clean Node.js", 388 | "due": "tomorrow", 389 | "done": false, 390 | "id": "f8d78d57-10fa-4565-9c1f-7ba435f800b1" 391 | }, 392 | { 393 | "what": "code castle", 394 | "due": "tomorrow", 395 | "done": false, 396 | "id": "32296241-9e13-4b5b-858a-52be96778377" 397 | }, 398 | { 399 | "what": "crash castle", 400 | "due": "tomorrow", 401 | "done": false, 402 | "id": "e36011dd-100c-4db2-8808-55fe4091ae00" 403 | }, 404 | { 405 | "what": "try to buy adults", 406 | "due": "tomorrow", 407 | "done": false, 408 | "id": "1b29b9f0-5f3c-42d6-937d-6f688dba6661" 409 | }, 410 | { 411 | "what": "pretend to make laptop", 412 | "due": "tomorrow", 413 | "done": false, 414 | "id": "1da7d7b1-152f-461f-8072-2b84290490db" 415 | }, 416 | { 417 | "what": "avoid castle", 418 | "due": "tomorrow", 419 | "done": false, 420 | "id": "20217340-ecc5-40ba-aac3-3b1385ca0681" 421 | }, 422 | { 423 | "what": "try to fix Node.js", 424 | "due": "tomorrow", 425 | "done": false, 426 | "id": "373b895d-8662-4d09-9da0-0a6d5d9ff115" 427 | }, 428 | { 429 | "what": "avoid throwing charges", 430 | "due": "tomorrow", 431 | "done": false, 432 | "id": "362f3256-295d-4baa-9821-4e5ece50f154" 433 | }, 434 | { 435 | "what": "try to tweet fishing rod", 436 | "due": "tomorrow", 437 | "done": false, 438 | "id": "9e721b88-58b0-4055-b216-7be02e6f8470" 439 | }, 440 | { 441 | "what": "add castle", 442 | "due": "tomorrow", 443 | "done": false, 444 | "id": "6ecd397f-34bb-4622-8eb3-a4d9058cbdf8" 445 | }, 446 | { 447 | "what": "throw laptop", 448 | "due": "tomorrow", 449 | "done": false, 450 | "id": "e4e8ab3f-3ede-4b31-b00b-a61abd730b84" 451 | }, 452 | { 453 | "what": "try to learn charges", 454 | "due": "tomorrow", 455 | "done": false, 456 | "id": "49384468-e893-4ada-8b1f-530776038946" 457 | }, 458 | { 459 | "what": "try to skip charges", 460 | "due": "tomorrow", 461 | "done": false, 462 | "id": "24a037d1-5136-4f72-9242-0c133bb5d5f3" 463 | }, 464 | { 465 | "what": "sell books", 466 | "due": "tomorrow", 467 | "done": false, 468 | "id": "076aba82-a0b5-45a5-959b-a9fca4799763" 469 | }, 470 | { 471 | "what": "throw knife", 472 | "due": "tomorrow", 473 | "done": false, 474 | "id": "7c3bcfcf-18b4-41a4-9f4a-9ad74905d282" 475 | }, 476 | { 477 | "what": "help throw Node.js", 478 | "due": "tomorrow", 479 | "done": false, 480 | "id": "67f8be37-3555-415c-9f34-0dd4b77e5481" 481 | }, 482 | { 483 | "what": "try to find chess", 484 | "due": "tomorrow", 485 | "done": false, 486 | "id": "9d62e22d-ea24-4e4a-a80c-fad106d16d4f" 487 | }, 488 | { 489 | "what": "try to add fishing rod", 490 | "due": "tomorrow", 491 | "done": false, 492 | "id": "ead32ebd-a5f7-4d2e-8e7a-5b822838133d" 493 | }, 494 | { 495 | "what": "skip finding Italian", 496 | "due": "tomorrow", 497 | "done": false, 498 | "id": "4c4bdaa2-3864-4e40-800b-6059d117dcd8" 499 | }, 500 | { 501 | "what": "avoid needle work", 502 | "due": "tomorrow", 503 | "done": false, 504 | "id": "ca87cbb2-fe5f-4a72-bf4f-dcc7016d9c46" 505 | }, 506 | { 507 | "what": "pretend to code boots", 508 | "due": "tomorrow", 509 | "done": false, 510 | "id": "21cb6d6c-d9c8-4fb0-b661-20a6dcbfeaf4" 511 | }, 512 | { 513 | "what": "forget adults", 514 | "due": "tomorrow", 515 | "done": false, 516 | "id": "4a565cff-f3a2-4956-9e01-b83a6739b9d9" 517 | }, 518 | { 519 | "what": "promote boots", 520 | "due": "tomorrow", 521 | "done": false, 522 | "id": "a0a258d8-24e7-4c76-9c2e-d1cca2ceaa1e" 523 | }, 524 | { 525 | "what": "avoid playing adults", 526 | "due": "tomorrow", 527 | "done": false, 528 | "id": "d4010122-4800-4e97-a739-f37f1fc38cfa" 529 | }, 530 | { 531 | "what": "make milk", 532 | "due": "tomorrow", 533 | "done": false, 534 | "id": "16ecb5c5-14fd-4554-98c2-07f53ae9c827" 535 | }, 536 | { 537 | "what": "fix chess", 538 | "due": "tomorrow", 539 | "done": false, 540 | "id": "a6c1ba29-b142-4bea-9e63-02484fc7a3d9" 541 | }, 542 | { 543 | "what": "throw knife", 544 | "due": "tomorrow", 545 | "done": false, 546 | "id": "dc77f679-1848-4a89-84cb-53aa377ac0da" 547 | }, 548 | { 549 | "what": "add Italian", 550 | "due": "tomorrow", 551 | "done": false, 552 | "id": "190f0191-af71-4fac-a467-58f276c14739" 553 | }, 554 | { 555 | "what": "pretend to code laptop", 556 | "due": "tomorrow", 557 | "done": false, 558 | "id": "73ad3df5-530c-4327-a0d2-296d5d1a8388" 559 | }, 560 | { 561 | "what": "try to avoid distant relatives", 562 | "due": "tomorrow", 563 | "done": false, 564 | "id": "8bd52ae0-0aac-48ea-b474-19951eb59166" 565 | }, 566 | { 567 | "what": "help learn boots", 568 | "due": "tomorrow", 569 | "done": false, 570 | "id": "d2eec216-285f-4e44-8c4c-6248592583f9" 571 | }, 572 | { 573 | "what": "add needle work", 574 | "due": "tomorrow", 575 | "done": false, 576 | "id": "42beb520-cacd-4b66-b740-85f2c83ab7c9" 577 | }, 578 | { 579 | "what": "help forget laptop", 580 | "due": "tomorrow", 581 | "done": false, 582 | "id": "18d31c9b-f2f9-4538-89ad-d0f3766a9830" 583 | }, 584 | { 585 | "what": "code fishing rod", 586 | "due": "tomorrow", 587 | "done": false, 588 | "id": "4a254ad7-ab70-4257-bc59-c0f2b5e61dae" 589 | }, 590 | { 591 | "what": "try to promote laptop", 592 | "due": "tomorrow", 593 | "done": false, 594 | "id": "82e54f4b-0507-4127-90a1-ab3a919790d7" 595 | }, 596 | { 597 | "what": "exercise laptop", 598 | "due": "tomorrow", 599 | "done": false, 600 | "id": "2cecf0c2-d0b1-4cf6-9ee2-d3ac87e18284" 601 | } 602 | ] -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Instant TodoMVC 7 | 8 | 9 | 10 | 11 |
12 |

Prehydrated, self-rewriting TodoMVC

13 |

Uses virtual-dom, 14 | build time hydration and 15 | self-rewriting using bottle-service. 16 | Tested in Chrome desktop, see 17 | browser instructions. 18 | Read the Instant web apps blog post, 19 | or see the demo recording.

20 |
21 | 22 | 23 |
24 |
25 |
26 |

todos

27 |
28 |
    29 |
  • 30 |
    31 |
  • 32 |
  • 33 |
    34 |
  • 35 |
  • 36 |
    37 |
  • 38 |
  • 39 |
    40 |
  • 41 |
  • 42 |
    43 |
  • 44 |
  • 45 |
    46 |
  • 47 |
  • 48 |
    49 |
  • 50 |
  • 51 |
    52 |
  • 53 |
  • 54 |
    55 |
  • 56 |
  • 57 |
    58 |
  • 59 |
  • 60 |
    61 |
  • 62 |
  • 63 |
    64 |
  • 65 |
  • 66 |
    67 |
  • 68 |
  • 69 |
    70 |
  • 71 |
  • 72 |
    73 |
  • 74 |
  • 75 |
    76 |
  • 77 |
  • 78 |
    79 |
  • 80 |
  • 81 |
    82 |
  • 83 |
  • 84 |
    85 |
  • 86 |
  • 87 |
    88 |
  • 89 |
  • 90 |
    91 |
  • 92 |
  • 93 |
    94 |
  • 95 |
  • 96 |
    97 |
  • 98 |
  • 99 |
    100 |
  • 101 |
  • 102 |
    103 |
  • 104 |
  • 105 |
    106 |
  • 107 |
  • 108 |
    109 |
  • 110 |
  • 111 |
    112 |
  • 113 |
  • 114 |
    115 |
  • 116 |
  • 117 |
    118 |
  • 119 |
  • 120 |
    121 |
  • 122 |
  • 123 |
    124 |
  • 125 |
  • 126 |
    127 |
  • 128 |
  • 129 |
    130 |
  • 131 |
  • 132 |
    133 |
  • 134 |
  • 135 |
    136 |
  • 137 |
  • 138 |
    139 |
  • 140 |
  • 141 |
    142 |
  • 143 |
  • 144 |
    145 |
  • 146 |
  • 147 |
    148 |
  • 149 |
  • 150 |
    151 |
  • 152 |
  • 153 |
    154 |
  • 155 |
  • 156 |
    157 |
  • 158 |
  • 159 |
    160 |
  • 161 |
  • 162 |
    163 |
  • 164 |
  • 165 |
    166 |
  • 167 |
  • 168 |
    169 |
  • 170 |
  • 171 |
    172 |
  • 173 |
  • 174 |
    175 |
  • 176 |
  • 177 |
    178 |
  • 179 |
  • 180 |
    181 |
  • 182 |
  • 183 |
    184 |
  • 185 |
  • 186 |
    187 |
  • 188 |
  • 189 |
    190 |
  • 191 |
  • 192 |
    193 |
  • 194 |
  • 195 |
    196 |
  • 197 |
  • 198 |
    199 |
  • 200 |
  • 201 |
    202 |
  • 203 |
  • 204 |
    205 |
  • 206 |
  • 207 |
    208 |
  • 209 |
  • 210 |
    211 |
  • 212 |
  • 213 |
    214 |
  • 215 |
  • 216 |
    217 |
  • 218 |
  • 219 |
    220 |
  • 221 |
  • 222 |
    223 |
  • 224 |
  • 225 |
    226 |
  • 227 |
  • 228 |
    229 |
  • 230 |
  • 231 |
    232 |
  • 233 |
  • 234 |
    235 |
  • 236 |
  • 237 |
    238 |
  • 239 |
  • 240 |
    241 |
  • 242 |
  • 243 |
    244 |
  • 245 |
  • 246 |
    247 |
  • 248 |
  • 249 |
    250 |
  • 251 |
  • 252 |
    253 |
  • 254 |
  • 255 |
    256 |
  • 257 |
  • 258 |
    259 |
  • 260 |
  • 261 |
    262 |
  • 263 |
  • 264 |
    265 |
  • 266 |
  • 267 |
    268 |
  • 269 |
  • 270 |
    271 |
  • 272 |
  • 273 |
    274 |
  • 275 |
  • 276 |
    277 |
  • 278 |
  • 279 |
    280 |
  • 281 |
  • 282 |
    283 |
  • 284 |
  • 285 |
    286 |
  • 287 |
  • 288 |
    289 |
  • 290 |
  • 291 |
    292 |
  • 293 |
  • 294 |
    295 |
  • 296 |
  • 297 |
    298 |
  • 299 |
  • 300 |
    301 |
  • 302 |
  • 303 |
    304 |
  • 305 |
  • 306 |
    307 |
  • 308 |
  • 309 |
    310 |
  • 311 |
  • 312 |
    313 |
  • 314 |
  • 315 |
    316 |
  • 317 |
  • 318 |
    319 |
  • 320 |
  • 321 |
    322 |
  • 323 |
  • 324 |
    325 |
  • 326 |
  • 327 |
    328 |
  • 329 |
330 |
331 | 337 |
338 |
339 | 340 | 343 | 344 | 345 | 346 | 347 | 348 | --------------------------------------------------------------------------------