├── .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 |
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 | 
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 |
--------------------------------------------------------------------------------