Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Add C2 todo example.

  • Loading branch information...
commit 358eb2a106bbd715ab3ef5a72daf81db226170e1 0 parents
@lynaghk authored
1  .gitignore
@@ -0,0 +1 @@
+.lein-cljsbuild*
7 README.markdown
@@ -0,0 +1,7 @@
+TodoFRP
+=======
+
+Functional reactive programming: like model view controller, except better because it's functional and stuff.
+
+In the same spirit of [TodoMVC](https://github.com/addyosmani/todomvc/), TodoFRP showcases different functional implementations of the exact same application so that you can decide for yourself what makes sense and what doesn't.
+
410 todo/assets/base.css
@@ -0,0 +1,410 @@
+html,
+body {
+ margin: 0;
+ padding: 0;
+}
+
+button {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ background: none;
+ font-size: 100%;
+ vertical-align: baseline;
+ font-family: inherit;
+ color: inherit;
+ -webkit-appearance: none;
+ /*-moz-appearance: none;*/
+ -ms-appearance: none;
+ -o-appearance: none;
+ appearance: none;
+}
+
+body {
+ font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ line-height: 1.4em;
+ background: #eaeaea url('bg.png');
+ color: #4d4d4d;
+ width: 550px;
+ margin: 0 auto;
+ -webkit-font-smoothing: antialiased;
+ -moz-font-smoothing: antialiased;
+ -ms-font-smoothing: antialiased;
+ -o-font-smoothing: antialiased;
+ font-smoothing: antialiased;
+}
+
+#todoapp {
+ background: #fff;
+ background: rgba(255, 255, 255, 0.9);
+ margin: 130px 0 40px 0;
+ border: 1px solid #ccc;
+ position: relative;
+ border-top-left-radius: 2px;
+ border-top-right-radius: 2px;
+ box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.2),
+ 0 25px 50px 0 rgba(0, 0, 0, 0.15);
+}
+
+#todoapp:before {
+ content: '';
+ border-left: 1px solid #f5d6d6;
+ border-right: 1px solid #f5d6d6;
+ width: 2px;
+ position: absolute;
+ top: 0;
+ left: 40px;
+ height: 100%;
+}
+
+#todoapp input::-webkit-input-placeholder {
+ font-style: italic;
+}
+
+#todoapp input:-moz-placeholder {
+ font-style: italic;
+ color: #a9a9a9;
+}
+
+#todoapp h1 {
+ position: absolute;
+ top: -120px;
+ width: 100%;
+ font-size: 70px;
+ font-weight: bold;
+ text-align: center;
+ color: #b3b3b3;
+ color: rgba(255, 255, 255, 0.3);
+ text-shadow: -1px -1px rgba(0, 0, 0, 0.2);
+ -webkit-text-rendering: optimizeLegibility;
+ -moz-text-rendering: optimizeLegibility;
+ -ms-text-rendering: optimizeLegibility;
+ -o-text-rendering: optimizeLegibility;
+ text-rendering: optimizeLegibility;
+}
+
+#header {
+ padding-top: 15px;
+ border-radius: inherit;
+}
+
+#header:before {
+ content: '';
+ position: absolute;
+ top: 0;
+ right: 0;
+ left: 0;
+ height: 15px;
+ z-index: 2;
+ border-bottom: 1px solid #6c615c;
+ background: #8d7d77;
+ background: -webkit-gradient(linear, left top, left bottom, from(rgba(132, 110, 100, 0.8)),to(rgba(101, 84, 76, 0.8)));
+ background: -webkit-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
+ background: -moz-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
+ background: -o-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
+ background: -ms-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
+ background: linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
+ filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670');
+ border-top-left-radius: 1px;
+ border-top-right-radius: 1px;
+}
+
+#new-todo,
+.edit {
+ position: relative;
+ margin: 0;
+ width: 100%;
+ font-size: 24px;
+ font-family: inherit;
+ line-height: 1.4em;
+ border: 0;
+ outline: none;
+ color: inherit;
+ padding: 6px;
+ 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;
+}
+
+#new-todo {
+ padding: 16px 16px 16px 60px;
+ border: none;
+ background: rgba(0, 0, 0, 0.02);
+ z-index: 2;
+ box-shadow: none;
+}
+
+#main {
+ position: relative;
+ z-index: 2;
+ border-top: 1px dotted #adadad;
+}
+
+label[for='toggle-all'] {
+ display: none;
+}
+
+#toggle-all {
+ position: absolute;
+ top: -56px;
+ left: -15px;
+ width: 65px;
+ height: 41px;
+ text-align: center;
+ border: none; /* Mobile Safari */
+ -webkit-appearance: none;
+ /*-moz-appearance: none;*/
+ -ms-appearance: none;
+ -o-appearance: none;
+ appearance: none;
+ -webkit-transform: rotate(90deg);
+ /*-moz-transform: rotate(90deg);*/
+ -ms-transform: rotate(90deg);
+ /*-o-transform: rotate(90deg);*/
+ transform: rotate(90deg);
+}
+
+#toggle-all:before {
+ content: '»';
+ font-size: 28px;
+ color: #d9d9d9;
+ padding: 0 25px 7px;
+}
+
+#toggle-all:checked:before {
+ color: #737373;
+}
+
+#todo-list {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+#todo-list li {
+ position: relative;
+ font-size: 24px;
+ border-bottom: 1px dotted #ccc;
+}
+
+#todo-list li:last-child {
+ border-bottom: none;
+}
+
+#todo-list li.editing {
+ border-bottom: none;
+ padding: 0;
+}
+
+#todo-list li.editing .edit {
+ display: block;
+ width: 506px;
+ padding: 13px 17px 12px 17px;
+ margin: 0 0 0 43px;
+}
+
+#todo-list li.editing .view {
+ display: none;
+}
+
+#todo-list li .toggle {
+ text-align: center;
+ width: 40px;
+ height: 40px;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ margin: auto 0;
+ border: none; /* Mobile Safari */
+ -webkit-appearance: none;
+ /*-moz-appearance: none;*/
+ -ms-appearance: none;
+ -o-appearance: none;
+ appearance: none;
+}
+
+#todo-list li .toggle:after {
+ font-size: 18px;
+ content: '';
+ line-height: 43px; /* 40 + a couple of pixels visual adjustment */
+ font-size: 20px;
+ color: #d9d9d9;
+ text-shadow: 0 -1px 0 #bfbfbf;
+}
+
+#todo-list li .toggle:checked:after {
+ color: #85ada7;
+ text-shadow: 0 1px 0 #669991;
+ bottom: 1px;
+ position: relative;
+}
+
+#todo-list li label {
+ word-break: break-word;
+ padding: 15px;
+ margin-left: 45px;
+ display: block;
+ 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;
+}
+
+#todo-list li.completed label {
+ color: #a9a9a9;
+ text-decoration: line-through;
+}
+
+#todo-list li .destroy {
+ display: none;
+ position: absolute;
+ top: 0;
+ right: 10px;
+ bottom: 0;
+ width: 40px;
+ height: 40px;
+ margin: auto 0;
+ font-size: 22px;
+ color: #a88a8a;
+ -webkit-transition: all 0.2s;
+ -moz-transition: all 0.2s;
+ -ms-transition: all 0.2s;
+ -o-transition: all 0.2s;
+ transition: all 0.2s;
+}
+
+#todo-list li .destroy:hover {
+ text-shadow: 0 0 1px #000,
+ 0 0 10px rgba(199, 107, 107, 0.8);
+ -webkit-transform: scale(1.3);
+ -moz-transform: scale(1.3);
+ -ms-transform: scale(1.3);
+ -o-transform: scale(1.3);
+ transform: scale(1.3);
+}
+
+#todo-list li .destroy:after {
+ content: '';
+}
+
+#todo-list li:hover .destroy {
+ display: block;
+}
+
+#todo-list li .edit {
+ display: none;
+}
+
+#todo-list li.editing:last-child {
+ margin-bottom: -1px;
+}
+
+#footer {
+ color: #777;
+ padding: 0 15px;
+ position: absolute;
+ right: 0;
+ bottom: -31px;
+ left: 0;
+ height: 20px;
+ z-index: 1;
+ text-align: center;
+}
+
+#footer:before {
+ content: '';
+ position: absolute;
+ right: 0;
+ bottom: 31px;
+ left: 0;
+ height: 50px;
+ z-index: -1;
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3),
+ 0 6px 0 -3px rgba(255, 255, 255, 0.8),
+ 0 7px 1px -3px rgba(0, 0, 0, 0.3),
+ 0 43px 0 -6px rgba(255, 255, 255, 0.8),
+ 0 44px 2px -6px rgba(0, 0, 0, 0.2);
+}
+
+#todo-count {
+ float: left;
+ text-align: left;
+}
+
+#filters {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ position: absolute;
+ right: 0;
+ left: 0;
+}
+
+#filters li {
+ display: inline;
+}
+
+#filters li a {
+ color: #83756f;
+ margin: 2px;
+ text-decoration: none;
+}
+
+#filters li a.selected {
+ font-weight: bold;
+}
+
+#clear-completed {
+ float: right;
+ position: relative;
+ line-height: 20px;
+ text-decoration: none;
+ background: rgba(0, 0, 0, 0.1);
+ font-size: 11px;
+ padding: 0 10px;
+ border-radius: 3px;
+ box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.2);
+}
+
+#clear-completed:hover {
+ background: rgba(0, 0, 0, 0.15);
+ box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.3);
+}
+
+#info {
+ margin: 65px auto 0;
+ color: #a6a6a6;
+ font-size: 12px;
+ text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7);
+ text-align: center;
+}
+
+#info a {
+ color: inherit;
+}
+
+/*
+ Hack to remove background from Mobile Safari.
+ Can't use it globally since it destroys checkboxes in Firefox and Opera
+*/
+@media screen and (-webkit-min-device-pixel-ratio:0) {
+ #toggle-all,
+ #todo-list li .toggle {
+ background: none;
+ }
+}
+
+.hidden{
+ display:none;
+}
BIN  todo/assets/bg.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 todo/c2/README.markdown
@@ -0,0 +1,15 @@
+C2 TodoFRP
+==========
+
+[C2](https://github.com/lynaghk/c2/) is a data visualization library for Clojure and ClojureScript.
+Under the hood, C2's `bind!` macro uses the [reflex](https://github.com/lynaghk/reflex) library to automatically setup watchers on mutable state contained within atoms.
+
+The state of the todo list is stored in two atoms in the `todo.core` namespace; one atom for the list items and another for the current view state (all, active, or completed).
+The `todo.list` namespace serves as a "view", binding the core state to the DOM and attaching event handlers.
+
+Build
+=====
+
+Run `lein cljsbuild once` to generate the JavaScript.
+Open `public/index.html` in your favorite browser.
+
14 todo/c2/project.clj
@@ -0,0 +1,14 @@
+(defproject todo "0.1.0"
+ :description "TodoFRP with C2"
+ :dependencies [[com.keminglabs/c2 "0.2.1"]]
+
+ :min-lein-version "2.0.0"
+ :source-paths ["src/cljs"]
+
+ :plugins [[lein-cljsbuild "0.2.10"]]
+
+ :cljsbuild {:builds
+ [{:source-path "src/cljs"
+ :compiler {:output-to "public/todo.js"
+ :pretty-print false
+ :optimizations :advanced}}]})
1  todo/c2/public/.gitignore
@@ -0,0 +1 @@
+todo.js
31 todo/c2/public/index.html
@@ -0,0 +1,31 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+ <title>ClojureScript+C2 • TodoMVC</title>
+ <link rel="stylesheet" href="../../assets/base.css">
+ </head>
+ <body>
+ <section id="todoapp">
+ <header id="header">
+ <h1>todos</h1>
+ <input id="new-todo" placeholder="What needs to be done?" autofocus>
+ </header>
+ <!-- This section should be hidden by default and shown when there are todos -->
+ <section id="main">
+ </section>
+ <!-- This footer should hidden by default and shown when there are todos -->
+ <footer id="footer">
+ </footer>
+ </section>
+ <footer id="info">
+ <p>Double-click to edit a todo</p>
+ <p>Template by <a href="http://github.com/sindresorhus">Sindre Sorhus</a></p>
+ <!-- Change this out with your name and url ↓ -->
+ <p>Created by <a href="http://github.com/lynaghk">Kevin Lynagh</a></p>
+ </footer>
+ <!-- Scripts here. Don't remove this ↓ -->
+ <script src="todo.js"></script>
+ </body>
+</html>
111 todo/c2/src/cljs/todo/core.cljs
@@ -0,0 +1,111 @@
+(ns todo.core
+ (:use-macros [c2.util :only [p pp]])
+ (:use [cljs.reader :only [read-string]]
+ [clojure.string :only [blank?]]))
+
+;;;;;;;;;;;;;;;;;;;;;
+;;Core application state
+
+(def !todos
+ "Todo list, implicitly key'd by :title"
+ (atom []))
+
+(def !filter
+ "Which todo items should be displayed: all, active, or completed?"
+ (atom :all))
+
+
+;;;;;;;;;;;;;;;;;;;;;
+;;"Routing"
+
+(defn update-filter!
+ "Updates filter according to current location hash"
+ []
+ (let [[_ loc] (re-matches #"#/(\w+)" (.-hash js/location))]
+ (reset! !filter (keyword loc))))
+
+(set! (.-onhashchange js/window) update-filter!)
+
+
+;;;;;;;;;;;;;;;;;;;
+;;Persistence
+
+(def ls-key "todos-c2")
+(defn save-todos! []
+ (aset js/localStorage ls-key
+ (prn-str (map #(dissoc % :editing?) ;;Don't save editing state
+ @!todos))))
+
+(defn load-todos! []
+ (reset! !todos
+ (if-let [saved-str (aget js/localStorage ls-key)]
+ (read-string saved-str)
+ [])))
+
+(add-watch !todos :save save-todos!)
+
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;Query/manipulate todos
+
+(defn todo-count
+ ([] (count @!todos))
+ ([completed?] (count (filter #(= completed? (% :completed?))
+ @!todos))))
+
+(defn clear-completed!
+ "Remove completed items from the todo list."
+ []
+ (swap! !todos #(remove :completed? %)))
+
+(defn replace-todo! [old new]
+ (swap! !todos #(replace {old new} %)))
+
+(defn check-todo!
+ "Mark an item as (un)completed."
+ [todo completed?]
+ (replace-todo! todo (assoc todo :completed? completed?)))
+
+(defn edit-todo!
+ "Mark an item as currently being editing"
+ [todo]
+ (replace-todo! todo (assoc todo :editing? true)))
+
+(defn check-all!
+ "Mark all items as (un)completed."
+ [completed?]
+ (swap! !todos (fn [todos]
+ (map #(assoc % :completed? completed?)
+ todos))))
+
+(defn title-exists?
+ "Is there already a todo with that title?"
+ [title]
+ (some #(= title %)
+ (map :title @!todos)))
+
+(defn add-todo!
+ "Add a new todo to the list."
+ [title]
+ (let [title (.trim title)]
+ (when (not (or (blank? title)
+ (title-exists? title)))
+ (swap! !todos conj {:title title :completed? false}))))
+
+(defn clear-todo!
+ "Remove a single todo from the list."
+ [todo]
+ (swap! !todos
+ (fn [todos]
+ (remove #(= todo %) todos))))
+
+
+;;;;;;;;;;;;;;;;;;;;;
+;;Lil' helpers
+
+(defn capitalize [string]
+ (str (.toUpperCase (.charAt string 0))
+ (.slice string 1)))
+
+(defn evt->key [e]
+ (get {13 :enter} (.-keyCode e)))
119 todo/c2/src/cljs/todo/list.cljs
@@ -0,0 +1,119 @@
+(ns todo.list
+ (:use-macros [c2.util :only [p pp bind!]])
+ (:use [c2.core :only [unify]])
+ (:require [todo.core :as core]
+ [c2.dom :as dom]
+ [c2.event :as event]
+ [clojure.string :as str]))
+
+
+(defn todo*
+ "Todo item template"
+ [t]
+ (let [{:keys [completed? title editing?]} t]
+ [:li {:class (str/join " " [(when completed? "completed")
+ (when editing? "editing")])}
+ [:div.view
+ [:input.toggle {:type "checkbox"
+ :properties {:checked completed?}}]
+ [:label title]
+ [:button.destroy]]
+ [:input.edit {:value title}]]))
+
+(bind! "#main"
+ [:section#main {:style {:display (when (zero? (core/todo-count)) "none")}}
+ [:input#toggle-all {:type "checkbox"
+ :properties {:checked (every? :completed? @core/!todos)}}]
+ [:label {:for "toggle-all"} "Mark all as complete"]
+ [:ul#todo-list (unify (case @core/!filter
+ :active (remove :completed? @core/!todos)
+ :completed (filter :completed? @core/!todos)
+ ;;default to showing all events
+ @core/!todos)
+ todo*)]])
+
+;;If the application state changes, check to see if it's because a todo is being edited.
+;;If so, focus on that input element.
+(add-watch core/!todos :focus-editing
+ (fn []
+ (if-let [$input (dom/select ".editing input.edit")]
+ (.focus $input))))
+
+(bind! "#footer"
+ [:footer#footer {:style {:display (when (zero? (core/todo-count)) "none")}}
+
+ (let [items-left (core/todo-count false)]
+ [:span#todo-count
+ [:b items-left]
+ (str " item" (if (= 1 items-left) "" "s") " left")])
+
+ [:ul#filters
+ (unify [:all :active :completed]
+ (fn [type]
+ [:li
+ [:a {:class (if (= type @core/!filter) "selected" "")
+ :href (str "#/" (name type))}
+ (core/capitalize (name type))]])
+ :force-update? true)]
+
+
+ [:button#clear-completed
+ {:style {:display (when (zero? (core/todo-count true)) "none")}}
+ "Clear completed (" (core/todo-count true) ")"]])
+
+
+;;;;;;;;;;;;;;;;;;;;;
+;;Todo event handlers
+
+(event/on "#todo-list" ".toggle" :click
+ (fn [d _ e]
+ (let [checked? (.-checked (.-target e))]
+ (core/check-todo! d checked?))))
+
+(event/on "#todo-list" ".destroy" :click
+ (fn [d] (core/clear-todo! d)))
+
+
+
+;;Editing
+(event/on "#todo-list" :dblclick
+ (fn [d] (core/edit-todo! d)))
+
+(let [edit-todo! (fn [d e]
+ (let [new-title (dom/val (.-target e))]
+ (if (= "" new-title)
+ (core/clear-todo! d)
+ (core/replace-todo! d
+ (-> d
+ (assoc :title new-title)
+ (dissoc :editing?))))))]
+
+ (event/on "#todo-list" ".edit" :blur
+ (fn [d _ e]
+ (edit-todo! d e))
+ ;;Blur events don't bubble up the DOM, so we need to tell the listener to grab 'em in the capture phase
+ :capture true)
+
+ (event/on "#todo-list" ".edit" :keypress
+ (fn [d _ e]
+ (when (= :enter (core/evt->key e))
+ (edit-todo! d e)))))
+
+
+;;;;;;;;;;;;;;;;;;;;;;;;
+;;Control event handlers
+
+(event/on-raw "#toggle-all" :click
+ (fn [e]
+ (let [checked? (.-checked (.-target e))]
+ (core/check-all! checked?))))
+
+(event/on-raw "#clear-completed" :click
+ core/clear-completed!)
+
+(let [$todo-input (dom/select "#new-todo")]
+ (event/on-raw $todo-input :keypress
+ (fn [e]
+ (when (= :enter (core/evt->key e))
+ (core/add-todo! (dom/val $todo-input))
+ (dom/val $todo-input "")))))
12 todo/c2/src/cljs/todo/main.cljs
@@ -0,0 +1,12 @@
+(ns todo.main
+ (:use [c2.event :only [on-load]])
+ (:require [todo.core :as core]))
+
+
+(defn main
+ "Init function to run on page load."
+ []
+ (core/load-todos!)
+ (core/update-filter!))
+
+(on-load main)
Please sign in to comment.
Something went wrong with that request. Please try again.