Permalink
Browse files

An initial DerbyJS version of TodoMVC spec. 'make run'

  • Loading branch information...
1 parent 99ebd59 commit aa54820edc84f98672d77a7dffe39d863cfd2e58 @absoludity committed Aug 2, 2012
@@ -0,0 +1,5 @@
+node_modules
+lib
+gen
+*.swp
+*.un~
@@ -0,0 +1,8 @@
+compile:
+ ./node_modules/coffee-script/bin/coffee -bw -o ./lib -c ./src
+
+run:
+ npm install
+ ./node_modules/coffee-script/bin/coffee -b -o ./lib -c ./src
+ node server.js
+
@@ -0,0 +1,23 @@
+# Derby.js TodoMVC app
+
+## Getting started
+
+[NodeJS](http://nodejs.org) (>= 0.8.0) is required to run this app.
+
+## Run the app
+`make run`
+
+This will install the dependencies locally, compile the coffeescript and run
+the demo server for you.
+
+## Play with the code
+In one window, run `make` which will continue to compile the coffeescript as
+you save changes. In a separate window run `node server.js` and open up the
+shown URL.
+
+
+## TODO
+ * PROBLEM: Add ie.js - I've added the include to the template, but it
+ obviously gets stripped out due to the comments, plus Nate mentioned that
+ there's work needing to be done for ie < 10.
+ * QUESTION: Check with derby folk whether there's a better way to do the filtering while still using a route, and other improvements.
@@ -0,0 +1,15 @@
+{
+ "name": "todomvc-derbyjs",
+ "description": "",
+ "version": "0.0.0",
+ "main": "./server.js",
+ "dependencies": {
+ "derby": "0.3.12",
+ "express": "3.0.0beta4",
+ "gzippo": ">=0.1.4"
+ },
+ "private": true,
+ "devDependencies": {
+ "coffee-script": ">=1.3.1"
+ }
+}
@@ -0,0 +1 @@
+require('derby').run(__dirname + '/lib/server', 3003);
@@ -0,0 +1,41 @@
+http = require 'http'
+path = require 'path'
+express = require 'express'
+gzippo = require 'gzippo'
+derby = require 'derby'
+todos = require '../todos'
+serverError = require './serverError'
+
+## SERVER CONFIGURATION ##
+
+expressApp = express()
+server = http.createServer expressApp
+module.exports = server
+
+store = derby.createStore
+ listen: server
+require('./queries')(store)
+
+ONE_YEAR = 1000 * 60 * 60 * 24 * 365
+root = path.dirname path.dirname __dirname
+publicPath = path.join root, 'public'
+
+expressApp
+ .use(express.favicon())
+ # Gzip static files and serve from memory
+ .use(gzippo.staticGzip publicPath, maxAge: ONE_YEAR)
+ # Gzip dynamically rendered content
+ .use(express.compress())
+
+ # Adds req.getModel method
+ .use(store.modelMiddleware())
+ # Creates an express middleware from the app's routes
+ .use(todos.router())
+ .use(expressApp.router)
+ .use(serverError root)
+
+
+## SERVER ONLY ROUTES ##
+
+expressApp.all '*', (req) ->
+ throw "404: #{req.url}"
@@ -0,0 +1,3 @@
+module.exports = (store) ->
+ store.query.expose 'todos', 'forGroup', (group) ->
+ @where('group').equals(group)
@@ -0,0 +1,18 @@
+derby = require 'derby'
+{isProduction} = derby.util
+
+module.exports = (root) ->
+ staticPages = derby.createStatic root
+
+ return (err, req, res, next) ->
+ return next() unless err?
+
+ console.log(if err.stack then err.stack else err)
+
+ ## Customize error handling here ##
+ message = err.message || err.toString()
+ status = parseInt message
+ if status is 404
+ staticPages.render '404', res, {url: req.url}, 404
+ else
+ res.send if 400 <= status < 600 then status else 500
@@ -0,0 +1,95 @@
+derby = require 'derby'
+{get, view, ready} = derby.createApp module
+
+derby.use(require '../../ui')
+
+# Define a rendering function to handle both /:groupName and
+# /:groupName/filterName.
+renderGroup = (page, model, {groupName, filterName}) ->
+ groupTodosQuery = model.query('todos').forGroup(groupName)
+ model.subscribe "groups.#{groupName}", groupTodosQuery, (err, group, groupTodos) ->
+ model.ref '_group', group
+ model.setNull('_group._id', groupName)
+
+ todoIds = group.at 'todoIds' or []
+
+ # The refList supports array methods, but it stores the todo values on an
+ # object by id. The todos are stored on the object
+ # 'groups.groupName.todos', and their order is stored in an array of ids at
+ # '_group.todoIds'
+ model.refList '_todoList', "groups.#{groupName}.todos", todoIds
+
+ # Create a reactive function that automatically keeps '_stats'
+ # updated with the number of remaining and completed todos.
+ model.fn '_stats', '_todoList', (list) ->
+ remaining = 0
+ completed = 0
+ if list
+ for todo in list
+ if todo?.completed
+ completed++
+ else
+ remaining++
+ return {
+ completed: completed,
+ remaining: remaining,
+ moreThanOne: remaining > 1,
+ }
+
+ filterName = filterName or 'all'
+ page.render 'todo',
+ filterName: filterName
+ groupName: groupName
+
+
+## ROUTES ##
+get '/', (page) ->
+ page.redirect '/' + parseInt(Math.random() * 1e9).toString(36)
+
+get '/:groupName', renderGroup
+get '/:groupName/:filterName', renderGroup
+
+ready (model) ->
+
+ list = model.at '_todoList'
+ group = model.at '_group'
+
+ group.on 'set', 'select_all', (select_all, previous_value, isLocal, e) ->
+ # Is there a way to do this with one call rather than iterating?
+ todos = model.at('_group.todos')
+ for item in list.get()
+ todos.set("#{item.id}.completed", select_all)
+
+ newTodo = model.at '_newTodo'
+ exports.add = ->
+ # Don't add a blank todo
+ return unless text = view.escapeHtml newTodo.get()
+ newTodo.set ''
+ # Insert the new todo before the first completed item in the list
+ # or append to the end if none are completed
+ items = list.get()
+ i = 0
+ if items
+ for todo, i in list.get()
+ break if todo.completed
+ list.insert i, {text:text, completed: false, group: model.get '_group.id'}
+
+ exports.del = (e) ->
+ # Derby extends model.at to support creation from DOM nodes
+ model.at(e.target).remove()
+
+ exports.clearCompleted = ->
+ completed_indexes = (i for item, i in list.get() when item.completed)
+ list.remove(i) for i in completed_indexes.reverse()
+ group.set('select_all', false)
+
+ exports.endEdit = (e) ->
+ item = model.at(e.target)
+ item.set('_editing', false)
+ text = item.get('text').trim()
+ if not text
+ item.remove()
+
+ exports.startEdit = (e) ->
+ item = model.at(e.target)
+ item.set('_editing', true)
@@ -0,0 +1 @@
+@import "./base";
@@ -0,0 +1,68 @@
+#todo-list li input.text {
+ word-break: break-word;
+ margin: 15px 15px 15px 60px;
+ line-height: 1.2;
+ -webkit-transition: color 0.4s;
+ -moz-transition: color 0.4s;
+ -ms-transition: color 0.4s;
+ -o-transition: color 0.4s;
+ transition: color 0.4s;
+ font-size: 24px;
+ color: inherit;
+ background-color: transparent;
+ display: block;
+ border: none;
+ width: 506px;
+ padding: 13px 17px 12px 17px;
+ margin: 0 0 0 43px;
+}
+#todo-list li input.text:focus {
+ border: 1px solid #999;
+ box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -ms-box-sizing: border-box;
+ -o-box-sizing: border-box;
+ box-sizing: border-box;
+ -webkit-font-smoothing: antialiased;
+ -moz-font-smoothing: antialiased;
+ -ms-font-smoothing: antialiased;
+ -o-font-smoothing: antialiased;
+ font-smoothing: antialiased;
+ outline: none;
+}
+
+#todo-list li.completed input.text {
+ color: #a9a9a9;
+ text-decoration: line-through;
+}
+
+section.empty-list, footer.empty-list {
+ display: none;
+}
+
+ul#todo-list.active li.completed {
+ display: none;
+}
+
+ul#todo-list.completed li.active {
+ display: none;
+}
+
+section.empty-list #toggle-all {
+ display: none;
+}
+button#clear-completed.non-completed {
+ display: none;
+}
+
+#filters.all li.all a,
+#filters.active li.active a,
+#filters.completed li.completed a {
+ font-weight: bold;
+}
+
+#todo-list li.editing input.toggle,
+#todo-list li.editing button.destroy {
+ display: none;
+}
@@ -0,0 +1,18 @@
+.connection {
+ position: absolute;
+ text-align: center;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 0;
+ z-index: 99;
+}
+.connection > .alert {
+ background: #fff1a8;
+ border: 1px solid #999;
+ border-top: 0;
+ border-radius: 0 0 3px 3px;
+ display: inline-block;
+ line-height: 21px;
+ padding: 0 12px;
+}
@@ -0,0 +1,22 @@
+<connectionAlert:>
+ <div class="connection">
+ <!--
+ connected and canConnect are built-in properties of model. If a variable
+ is not defined in the current context, it will be looked up in the model
+ data and the model properties
+ -->
+ {#unless connected}
+ <p class="alert">
+ {#if canConnect}
+ <!-- Leading space is removed, and trailing space is maintained -->
+ Offline
+ <!-- a :self path alias is automatically created per component -->
+ {#unless :self.hideReconnect}
+ &ndash; <a x-bind="click:connect">Reconnect</a>
+ {/}
+ {else}
+ Unable to reconnect &ndash; <a x-bind="click:reload">Reload</a>
+ {/}
+ </p>
+ {/}
+ </div>
@@ -0,0 +1,13 @@
+exports.connect = function() {
+ model = this.model
+ // Hide the reconnect link for a second after clicking it
+ model.set('hideReconnect', true)
+ setTimeout(function() {
+ model.set('hideReconnect', false)
+ }, 1000)
+ model.socket.socket.connect()
+}
+
+exports.reload = function() {
+ window.location.reload()
+}
@@ -0,0 +1,14 @@
+var config = {
+ filename: __filename
+, styles: '../styles/ui'
+, scripts: {
+ connectionAlert: require('./connectionAlert')
+ }
+};
+
+module.exports = ui
+ui.decorate = 'derby'
+
+function ui(derby, options) {
+ derby.createLibrary(config, options)
+}
@@ -0,0 +1,16 @@
+<!--
+ This is a static template file, so it doesn't have an associated app.
+ It is rendered by the server via a staticPages renderer.
+
+ Since static pages don't include the Derby client library, they can't have
+ bound variables that automatically update. However, they do support initial
+ template tag rendering from a context object and/or model.
+-->
+
+<Title:>
+ Not found
+
+<Body:>
+ <h1>404</h1>
+ <p>Sorry, we can't find anything at <b>{{url}}</b>.
+ <p>Try heading back to the <a href="/">home page</a>.
Oops, something went wrong.

0 comments on commit aa54820

Please sign in to comment.