├── .gitignore ├── component.json ├── client ├── item │ ├── component.json │ └── index.js ├── item-presenter │ ├── template.html │ ├── component.json │ ├── style.css │ └── index.js └── boot │ ├── component.json │ ├── style.css │ └── index.js ├── Makefile ├── package.json ├── History.md ├── index.html ├── app.js ├── server └── items.js └── Readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | components 2 | build 3 | node_modules 4 | -------------------------------------------------------------------------------- /component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo", 3 | "description": "TodoMVC component example", 4 | "version": "1.0.0", 5 | "locals": ["boot"], 6 | "paths": ["client"] 7 | } -------------------------------------------------------------------------------- /client/item/component.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Item model", 3 | "dependencies": { 4 | "component/model": "*", 5 | "component/model-timestamps": "*" 6 | }, 7 | "scripts": [ 8 | "index.js" 9 | ] 10 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: components $(SRC) $(TEMPLATES) 2 | npm run component-build 3 | node app 4 | 5 | components: 6 | npm run component-install 7 | 8 | clean: 9 | rm -fr build components $(TEMPLATES) 10 | 11 | .PHONY: clean 12 | -------------------------------------------------------------------------------- /client/item-presenter/template.html: -------------------------------------------------------------------------------- 1 |
  • 2 | 3 | 4 | 5 |
  • 6 | -------------------------------------------------------------------------------- /client/item-presenter/component.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "component/classes": "*", 4 | "component/reactive": "*" 5 | }, 6 | "scripts": [ 7 | "index.js" 8 | ], 9 | "styles": [ 10 | "style.css" 11 | ], 12 | "templates": [ 13 | "template.html" 14 | ] 15 | } -------------------------------------------------------------------------------- /client/boot/component.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "visionmedia/page.js": "*", 4 | "component/collection": "*", 5 | "component/keyname": "*" 6 | }, 7 | "scripts": ["index.js"], 8 | "styles": ["style.css"], 9 | "locals": [ 10 | "item", 11 | "item-presenter" 12 | ] 13 | } -------------------------------------------------------------------------------- /client/item/index.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var model = require('model') 7 | , timestamps = require('model-timestamps'); 8 | 9 | /** 10 | * Item model. 11 | */ 12 | 13 | module.exports = model('Item') 14 | .attr('id') 15 | .attr('title') 16 | .attr('complete') 17 | .use(timestamps) 18 | .route('/items') 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo", 3 | "private": true, 4 | "version": "1.0.0", 5 | "dependencies": { 6 | "body-parser": "^1.9.2", 7 | "component": "^1.1.0", 8 | "express": "^4.10.1", 9 | "morgan": "^1.4.1" 10 | }, 11 | "scripts": { 12 | "component-install": "component-install", 13 | "component-build": "component-build" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/component/todo.git" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 1.0.0 / 2014-11-01 2 | ================== 3 | 4 | * use __component/reactive__ directly instead of __component/view__ (broken with latest reactive version) 5 | * rename __ItemView__ to __ItemPresenter__ 6 | * update to express 4 7 | * refactor due to component (1) 8 | * remove `name` and `version` properties of locals 9 | * simplify build process 10 | * use `templates` property instead of component convert 11 | * remove on the fly build (replace with [component-middleware](https://github.com/componentjs/middleware.js) in next version) -------------------------------------------------------------------------------- /client/item-presenter/style.css: -------------------------------------------------------------------------------- 1 | 2 | .item { 3 | margin: 0; 4 | padding: 5px 0; 5 | list-style: none; 6 | border-bottom: 1px dotted #eee; 7 | } 8 | 9 | .item:hover { 10 | background: #fbfbfb; 11 | } 12 | 13 | .item .x { 14 | color: #acacac; 15 | font-size: 10px; 16 | text-decoration: none; 17 | float: right; 18 | border-radius: 3px; 19 | margin-right: 2px; 20 | padding: 3px 5px; 21 | } 22 | 23 | .item .x:hover { 24 | background: #eee; 25 | } 26 | 27 | .item.complete label { 28 | text-decoration: line-through; 29 | color: #acacac; 30 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Todo 5 | 6 | 7 | 8 |
    9 |

    Todo

    10 |

    11 | 16 | 17 |
    18 | 19 | 22 | 23 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var express = require('express') 7 | , items = require('./server/items') 8 | , bodyParser = require('body-parser') 9 | , morgan = require('morgan') 10 | , app = express(); 11 | 12 | // configure 13 | 14 | app.use(morgan('dev')); 15 | app.use(bodyParser.json()); 16 | app.use(express.static(__dirname + '/build')); 17 | 18 | // items 19 | 20 | app.get('/items', items.all); 21 | app.post('/items', items.create); 22 | app.put('/items/:id', items.update); 23 | app.delete('/items/:id', items.remove); 24 | 25 | // catch-all 26 | 27 | app.get('/*', function(req, res){ 28 | res.sendFile(__dirname + '/index.html'); 29 | }); 30 | 31 | // bind 32 | 33 | app.listen(3000); 34 | console.log('listening on http://localhost:3000'); 35 | -------------------------------------------------------------------------------- /client/boot/style.css: -------------------------------------------------------------------------------- 1 | 2 | * { 3 | box-sizing: border-box; 4 | } 5 | 6 | body { 7 | padding: 50px; 8 | font: 14px Helvetica; 9 | } 10 | 11 | input[type=text] { 12 | padding: 10px; 13 | width: 400px; 14 | border-radius: 3px; 15 | border: 1px solid #eee; 16 | border-top-color: #ddd; 17 | } 18 | 19 | ul { 20 | margin: 0; 21 | padding: 0; 22 | width: 400px; 23 | } 24 | 25 | #content { 26 | width: 400px; 27 | position: relative; 28 | } 29 | 30 | #links { 31 | position: absolute; 32 | top: 5px; 33 | right: 0; 34 | font-size: 12px; 35 | } 36 | 37 | #links a { 38 | display: inline-block; 39 | text-decoration: none; 40 | margin: 0 3px; 41 | color: #0dbdff; 42 | } 43 | 44 | #links a:hover { 45 | color: #29cbff; 46 | border-bottom: 1px solid #ddd; 47 | } -------------------------------------------------------------------------------- /server/items.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Faux db. 4 | */ 5 | 6 | var items = []; 7 | 8 | /** 9 | * Index of `id` in db. 10 | */ 11 | 12 | function indexOf(id) { 13 | for (var i = 0, len = items.length; i < len; ++i) { 14 | if (id == items[i].id) { 15 | return i; 16 | } 17 | } 18 | } 19 | 20 | /** 21 | * GET all items. 22 | */ 23 | 24 | exports.all = function(req, res){ 25 | res.send(items); 26 | }; 27 | 28 | /** 29 | * POST a new item. 30 | */ 31 | 32 | exports.create = function(req, res){ 33 | var item = req.body; 34 | var id = items.push(item) - 1; 35 | item.id = id; 36 | res.send({ id: id }); 37 | }; 38 | 39 | /** 40 | * DELETE item :id. 41 | */ 42 | 43 | exports.remove = function(req, res){ 44 | var id = req.params.id; 45 | var i = indexOf(id); 46 | items.splice(i, 1); 47 | res.sendStatus(200); 48 | }; 49 | 50 | /** 51 | * PUT changes to item :id. 52 | */ 53 | 54 | exports.update = function(req, res){ 55 | var id = req.params.id; 56 | var i = indexOf(id); 57 | var body = req.body; 58 | var item = items[i]; 59 | if (!item) return res.send(404, 'item does not exist'); 60 | item.title = body.title; 61 | item.complete = body.complete; 62 | res.sendStatus(200); 63 | }; -------------------------------------------------------------------------------- /client/item-presenter/index.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var template = require('./template') 7 | , classes = require('classes') 8 | , reactive = require('reactive'); 9 | 10 | /** 11 | * Expose `ItemPresenter`. 12 | */ 13 | 14 | module.exports = ItemPresenter; 15 | 16 | /** 17 | * Initialize a new presenter for `item`. 18 | * 19 | * @param {Item} item 20 | * @api public 21 | */ 22 | 23 | function ItemPresenter(item) { 24 | this.item = item; 25 | this.view = reactive(template, item, { 26 | delegate: this 27 | }); 28 | this.classes = classes(this.view.el); 29 | this.toggleCompleteClass(); // update for initial template 30 | } 31 | 32 | /** 33 | * Save completed state change 34 | */ 35 | 36 | ItemPresenter.prototype.change = function(e){ 37 | var complete = e.target.checked; 38 | this.item.complete(complete); 39 | this.toggleCompleteClass(); 40 | this.item.save(); 41 | }; 42 | 43 | ItemPresenter.prototype.toggleCompleteClass = function(){ 44 | if (this.item.complete()) { 45 | this.classes.add('complete'); 46 | } else { 47 | this.classes.remove('complete'); 48 | } 49 | }; 50 | 51 | /** 52 | * Remove the item. 53 | */ 54 | 55 | ItemPresenter.prototype.remove = function(){ 56 | this.view.destroy(); 57 | this.item.destroy(); 58 | 59 | }; 60 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | ![](http://f.cl.ly/items/3l0r2s1C0d1Y1d3N202J/todo.png) 3 | 4 | Todo list component example app: 5 | 6 | ## Installation 7 | 8 | Of course have `component(1)` installed: 9 | 10 | $ npm install -g component 11 | 12 | Install express for the server, and the component dependencies: 13 | 14 | $ npm install 15 | $ make 16 | 17 | ## Implementation 18 | 19 | In this implementation all private client-side components are located in `./client`, 20 | while server related REST end-points are in `./server`. The `./index.html` 21 | file bootstraps the client-side, and `app.js` is a small Express server 22 | to power the backend. 23 | 24 | Each client-side component in `./client` defines its own dependencies, 25 | both "local" (in the `./client` dir), and remote from public components 26 | that devs have created. 27 | 28 | This is just _one_ example of how you could structure an application. You could 29 | for example take a more traditional approach with `./models`, `./controllers`, 30 | and `./views` etc. The entire app could be a single component, with all dependencies 31 | specified in the root ./component.json, however I recommend splitting your app 32 | into multiple as shown here, regardless of directory structure. 33 | 34 | ## Components used 35 | 36 | - [page.js](https://github.com/visionmedia/page.js) for routing 37 | - [model](https://github.com/component/model) for models 38 | - [collection](https://github.com/component/collection) for model collections 39 | - [keyname](https://github.com/component/keyname) for keycode name strings 40 | - [reactive](https://github.com/component/reactive) for reactive templates 41 | 42 | ## License 43 | 44 | MIT 45 | -------------------------------------------------------------------------------- /client/boot/index.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var Item = require('item') 7 | , ItemPresenter = require('item-presenter') 8 | , Collection = require('collection') 9 | , keyname = require('keyname') 10 | , page = require('page'); 11 | 12 | /** 13 | * Collection of todo Items. 14 | */ 15 | 16 | var items = new Collection; 17 | 18 | /** 19 | * Todo input. 20 | */ 21 | 22 | var input = document.querySelector('[name=todo]'); 23 | 24 | /** 25 | * Todo list. 26 | */ 27 | 28 | var list = document.querySelector('#todos'); 29 | 30 | /** 31 | * Handle keydown. 32 | */ 33 | 34 | input.onkeydown = function(e){ 35 | switch (keyname(e.which)) { 36 | case 'enter': 37 | // input 38 | var str = e.target.value; 39 | if ('' == str.trim()) return; 40 | e.target.value = ''; 41 | 42 | // item 43 | var item = new Item({ title: str }); 44 | items.push(item); 45 | item.save(); 46 | 47 | // presenter 48 | var presenter = new ItemPresenter(item); 49 | list.appendChild(presenter.view.el); 50 | break; 51 | } 52 | }; 53 | 54 | /** 55 | * Clear list. 56 | */ 57 | 58 | page('*', function(ctx, next){ 59 | list.innerHTML = ''; 60 | next(); 61 | }); 62 | 63 | /** 64 | * All items. 65 | */ 66 | 67 | page('/', function(){ 68 | Item.all(function(err, items){ 69 | items.each(function(item){ 70 | var presenter = new ItemPresenter(item); 71 | list.appendChild(presenter.view.el); 72 | }); 73 | }); 74 | }); 75 | 76 | /** 77 | * Completed items. 78 | */ 79 | 80 | page('/complete', function(){ 81 | Item.all(function(err, items){ 82 | items.select('complete()').each(function(item){ 83 | var presenter = new ItemPresenter(item); 84 | list.appendChild(presenter.view.el); 85 | }); 86 | }); 87 | }); 88 | 89 | /** 90 | * Incomplete items. 91 | */ 92 | 93 | page('/incomplete', function(){ 94 | Item.all(function(err, items){ 95 | items.reject('complete()').each(function(item){ 96 | var presenter = new ItemPresenter(item); 97 | list.appendChild(presenter.view.el); 98 | }); 99 | }); 100 | }); 101 | 102 | page(); 103 | --------------------------------------------------------------------------------