Initial version

This commit is contained in:
Dan Holmsand 2013-12-16 23:19:36 +01:00
commit 12566ced7b
36 changed files with 61085 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
target
pom.xml
.lein-repl-history

63
Makefile Normal file
View File

@ -0,0 +1,63 @@
PORT = 4562
PROF = dev
PROF = dev,test
# PROF = dev,test,srcmap
# PROF = prod,test
# PROF = prod
CLJSBUILD = client
CLJSDIRS = src test
VERSION = 0.0.1-SNAPSHOT
all: buildrun
run: openbrowser buildrun
leinbuild: setup
lein -o cljsbuild once $(CLJSBUILD)
openbrowser:
(sleep 1 && open -a "Google Chrome" site/test.html) &
buildrun: setup
lein -o with-profile $(PROF) cljsbuild auto $(CLJSBUILD)
install: setup
lein install
preclean:
rm -rf repl .repl target
clean: preclean
lein -o clean
setup: preclean
gen-react: bower_components
node bin/gencljs.js
# ./bin/gencljs.sh bower_components/react/react-with-addons.js cloact.react > gentmp; mv gentmp src/cloact/react.cljs
show-outdated:
lein ancient :all
veryclean: clean
rm -rf bower_components
bower_components:
bower install react#v0.5.1
setversion:
find . -name project.clj | \
xargs -n1 sed -i "" -e 's,\(cloact "\)\([^"]*\)",\1'$(VERSION)'"',g
tag: setversion
if git rev-parse v$(VERSION) 2>/dev/null; then \
echo "Tag already exists"; \
exit 1; \
else \
git commit --allow-empty -a -m"Version "$(VERSION); \
git tag v$(VERSION); \
fi

114
bin/gencljs.js Normal file
View File

@ -0,0 +1,114 @@
#! /usr/bin/env node
/*
This is a huge hack: convert React from js to cljs by embedding
in (js* ), and making it safe for Closure advanced optimization
by adding a lot of @expose annotations.
*/
var fs = require('fs');
var ns = "cloact.react";
var destFile = "src/cloact/React.cljs";
var srcfile = "bower_components/react/react-with-addons.js";
var React = require("../" + srcfile);
var src = "" + fs.readFileSync(srcfile);
// Names that might clash with module names
// XXX: meta might be broken now
var skipNames = ['var', 'object', 'base', 'map', 'meta', 'source', 'time'];
// Property names from DefaultDOMPropertyConfig.js
// https://github.com/facebook/react/blob/master/src/dom/DefaultDOMPropertyConfig.js
var propNames = ['allowFullScreen', 'autoComplete', 'autoFocus', 'autoPlay',
'charSet', 'encType', 'icon', 'preload', 'radioGroup', 'role',
'spellCheck', 'wmode',
'autoCapitalize',
'cx', 'cy', 'd', 'fx', 'fy', 'gradientTransform',
'gradientUnits', 'points', 'r', 'rx', 'ry', 'spreadMethod',
'stopColor', 'stopOpacity', 'strokeLinecap', 'strokeWidth',
'viewBox', 'x1', 'x2', 'x', 'y1', 'y2', 'y',
'componentConstructor', 'displayName'
];
var getNames = function (obj) {
var res = [];
for (var x in obj) {
res.push(x);
}
return res;
};
var stripAnnotations = function (src) {
// Stop bloody google closure complaining about jsdoc tags
// by removing "@" in block comments.
return src.replace(/\/\*([^*]|\*(?!\/))*\*\//gm, function (s) {
return s.replace(/@/g, '');
});
}
var quote = function (src) {
return src.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
}
var getLiteralKeys = function (src) {
var res = {};
src.replace(/([a-zA-Z$_][a-zA-Z0-9$_]*):/gm, function (s, key) {
res[key] = true;
});
return getNames(res);
}
var eventNames = function (keys) {
return keys.filter(function (x) {
return x.match(/^on[A-Z]/);
});
}
var ReactCompositeComponentInterface = {
mixins: null,
propTypes: null,
getDefaultProps: null,
getInitialState: null,
render: null,
componentWillMount: null,
componentDidMount: null,
componentWillReceiveProps: null,
shouldComponentUpdate: null,
componentWillUpdate: null,
componentDidUpdate: null,
componentWillUnmount: null,
updateComponent: null
}
var printCljs = function () {
var stripped = stripAnnotations(src);
var quoted = quote(stripped);
var domNames = getNames(React.DOM);
var evNames = eventNames(getLiteralKeys(src));
var iNames = getNames(ReactCompositeComponentInterface);
var names = [].concat(domNames, evNames, iNames, propNames);
var fnames = names.filter(function (n) {
return skipNames.indexOf(n) == -1;
});
res = ["(ns " + ns + ")",
'(js* "',
'/**',
' * @fileoverview React.js packaged for clojurescript',
' * @suppress {nonStandardJsDocs|checkRegExp}',
' */',
quoted,
'(function () {',
'var X = {};'].concat(fnames.map(function (x) {
return '/** @expose */\nX.' + x + " = true;"
})).concat([
'})();',
ns + ".React = (typeof(window) != 'undefined' ? window.React : global.React);",
'")',
]);
return res.join('\n');
}
fs.writeFileSync(destFile, printCljs());

62
bin/gencljs.sh Executable file
View File

@ -0,0 +1,62 @@
#! /bin/bash
src=$1
ns=$2
function skipkeywords () {
local kw=(break case catch continue debugger default delete do else
finally for function if in instanceof new return switch
this throw try typeof var void while with undefined null
class enum export extends import super
implements interface let package private protected public static
yield true false long char boolean string apply call prototype
constructor contains concat bind base array drop get list count isArray
map key max min meta create name object repeat set type core trim now
some sort splice split slice remove pop offset log js filter extend
reverse str join keys length test first replace cons
charAt charCodeAt)
local kws=${kw[*]}
local kwsplit=${kws// /\\|}
local keywords="^\\($kwsplit\\)$"
grep -v "$keywords"
}
function propnames() {
(# grep -o '\.[a-zA-Z$][a-zA-Z0-9_$]*' $1;
grep -o '[a-zA-Z$][a-zA-Z0-9$_]*:' $1) |
sed 's,[:.],,g' | sort | skipkeywords | uniq
}
function genexterns() {
echo "(function() {var X = function(){};"
cat "$1" | propnames |
# sed 's,\(.*\),/** @expose */\\nX.\1 = function () {};,'
sed 's,\(.*\),/** @expose */\\nX.\1 = true;,'
echo "})();"
}
function quote() {
sed -e 's,\\,\\\\,g' -e 's,",\\",g'
}
function skipdockeywords () {
sed "s,^[ ]*[*] *@[a-zA-Z].*,,"
}
function printns() {
echo "(ns $ns)"
}
function printjs() {
echo -n "(js* \""
cat "$1" | quote | skipdockeywords
genexterns "$1" "$ns"
echo "$ns.React = (typeof(window) != 'undefined' ? window.React : global.React);"
# echo "if (typeof(window) != 'undefined') window['React'] = $ns.React; else global['React'] = $ns.React;"
echo "\")"
}
printns
printjs "$src"
# Could also do something like

16
bower_components/react/.bower.json vendored Normal file
View File

@ -0,0 +1,16 @@
{
"name": "react",
"version": "0.5.1",
"main": "react.js",
"homepage": "https://github.com/facebook/react-bower",
"_release": "0.5.1",
"_resolution": {
"type": "version",
"tag": "v0.5.1",
"commit": "a05207f36d769b99e6b594f71671ca5bd2514435"
},
"_source": "git://github.com/facebook/react-bower.git",
"_target": "v0.5.1",
"_originalSource": "react",
"_direct": true
}

13317
bower_components/react/JSXTransformer.js vendored Normal file

File diff suppressed because one or more lines are too long

5
bower_components/react/bower.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"name": "react",
"version": "0.5.1",
"main": "react.js"
}

15332
bower_components/react/react-with-addons.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

14294
bower_components/react/react.js vendored Normal file

File diff suppressed because it is too large Load Diff

20
bower_components/react/react.min.js vendored Normal file

File diff suppressed because one or more lines are too long

20
examples/simple/Makefile Normal file
View File

@ -0,0 +1,20 @@
PROF = dev
# PROF = dev,test,srcmap
# PROF = prod,test
# PROF = prod
CLJSBUILD = client
all: autocompile
run: openbrowser autocompile
openbrowser:
(sleep 1 && open example.html) &
autocompile:
lein with-profile $(PROF) cljsbuild auto $(CLJSBUILD)
clean:
lein -o clean

View File

@ -0,0 +1,15 @@
div, h1, input {
font-family: HelveticaNeue, Helvetica;
}
.example-clock {
font-size: 128px;
line-height: 128px;
font-family: HelveticaNeue-UltraLight, Helvetica;
}
.color-input, .color-input input {
font-size: 24px;
line-height: 24px;
color: #444;
}

View File

@ -0,0 +1,15 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>cloact simple example</title>
<link rel="stylesheet" href="todos.css">
</head>
<body>
<h1>This will become an example when the ClojureScript is compiled</h1>
<script type="text/javascript" src="target/client.js"></script>
<script type="text/javascript">
example.run();
</script>
</body>
</html>

View File

@ -0,0 +1,25 @@
(defproject simple-cloact "0.0.1-SNAPSHOT"
:dependencies [[org.clojure/clojurescript "0.0-2120"]
[cloact "0.0.1-SNAPSHOT"]]
:plugins [[lein-cljsbuild "1.0.0"]]
:hooks [leiningen.cljsbuild]
:profiles {:prod {:cljsbuild
{:builds
{:client {:compiler
{:optimizations :advanced
:pretty-print false}}}}}
:srcmap {:cljsbuild
{:builds
{:client {:compiler
{:source-map "target/client.js.map"
:source-map-path "client"}}}}}}
:source-paths ["src"]
:cljsbuild
{:builds
{:client {:source-paths ["src"]
:compiler
{:output-dir "target/client"
:output-to "target/client.js"
:pretty-print true}}}})

View File

@ -0,0 +1,38 @@
(ns simpleexample
(:require [cloact.core :as cloact :refer [atom partial]]
[cloact.debug :refer-macros [dbg]]
[clojure.string :as string]))
(def timer (atom (js/Date.)))
(def time-color (atom "#f34"))
(defn update-time [time]
(js/setTimeout #(reset! time (js/Date.)) 100))
(defn greeting [props]
[:h1 (:message props)])
(defn clock []
(update-time timer)
(let [time-str (-> @timer .toTimeString (string/split " ") first)]
[:div.example-clock
{:style {:color @time-color}}
time-str]))
(defn color-input []
[:div.color-input
"Time color: "
[:input {:type "text"
:value @time-color
:on-change #(reset! time-color (-> % .-target .-value))}]])
(defn simple-example []
[:div
[greeting {:message "Hello world, it is now"}]
[clock]
[color-input]])
(defn ^:export run []
(cloact/render-component [simple-example]
(.-body js/document)))

20
examples/todomvc/Makefile Normal file
View File

@ -0,0 +1,20 @@
PROF = dev
# PROF = dev,test,srcmap
# PROF = prod,test
# PROF = prod
CLJSBUILD = client
all: autocompile
run: openbrowser autocompile
openbrowser:
(sleep 1 && open todomvc.html) &
autocompile:
lein with-profile $(PROF) cljsbuild auto $(CLJSBUILD)
clean:
lein -o clean

View File

@ -0,0 +1,25 @@
(defproject todomvc-cloact "0.0.1-SNAPSHOT"
:dependencies [[org.clojure/clojurescript "0.0-2120"]
[cloact "0.0.1-SNAPSHOT"]]
:plugins [[lein-cljsbuild "1.0.0"]]
:hooks [leiningen.cljsbuild]
:profiles {:prod {:cljsbuild
{:builds
{:client {:compiler
{:optimizations :advanced
:pretty-print false}}}}}
:srcmap {:cljsbuild
{:builds
{:client {:compiler
{:source-map "target/client.js.map"
:source-map-path "client"}}}}}}
:source-paths ["src"]
:cljsbuild
{:builds
{:client {:source-paths ["src"]
:compiler
{:output-dir "target/client"
:output-to "target/client.js"
:pretty-print true}}}})

View File

@ -0,0 +1,115 @@
(ns todomvc
(:require [cloact.core :as cloact :refer [atom partial]]
[cloact.debug :refer-macros [dbg]]))
(defn todo-input-render [{:keys [title on-save on-stop]}]
(let [val (atom title)
stop (fn []
(reset! val "")
(if on-stop (on-stop)))
save (fn []
(let [v (-> @val str clojure.string/trim)]
(if-not (empty? v) (on-save v))
(stop)))]
(fn [props]
(let [p {:type "text" :value @val :on-blur save
:on-change #(reset! val (-> % .-target .-value))
:on-key-up #(case (.-which %)
13 (save)
27 (stop)
nil)}]
[:input (cloact/merge-props props p)]))))
(def todo-input (with-meta todo-input-render
{:component-did-mount #(.focus (:dom-node %))}))
(defn todo-item [{:keys [todo editing is-editing
on-toggle on-save on-destroy]}]
(dbg "rendering item")
(let [{:keys [id done title]} todo]
[:li {:class (str (if done "completed ") (if is-editing "editing"))}
[:div.view
[:input.toggle {:type "checkbox" :checked done :on-change on-toggle}]
[:label {:on-double-click #(reset! editing id)} title]
[:button.destroy {:on-click on-destroy}]]
(when is-editing
[todo-input {:class "edit" :title title :on-save on-save
:on-stop #(reset! editing nil)}])]))
(defn todo-stats [{:keys [filter clear]}]
(let [props-for (fn [name]
{:class (when (= name @filter) "selected")
:on-click #(reset! filter name)})]
(fn [{:keys [active done]}]
[:div
[:span#todo-count
[:strong active] " " (case active 1 "item" "items") " left"]
[:ul#filters
[:li [:a (props-for :all) "All"]]
[:li [:a (props-for :active) "Active"]]
[:li [:a (props-for :done) "Completed"]]]
(when (pos? done)
[:button#clear-completed {:on-click clear}
"Clear completed " done])])))
(def counter (atom 0))
(defn add-todo [todos text]
(let [id (swap! counter inc)]
(swap! todos assoc id {:id id :title text :done false})))
(defn toggle [todos id] (swap! todos update-in [id :done] not))
(defn save [todos id title] (swap! todos assoc-in [id :title] title))
(defn delete [todos id] (swap! todos dissoc id))
(defn mod-map [m f a] (->> m (f a) (into (empty m))))
(defn complete-all [todos v] (swap! todos mod-map map #(assoc-in % [1 :done] v)))
(defn clear-done [todos] (swap! todos mod-map remove #(get-in % [1 :done])))
(defn todo-main [props]
(let [todos (or (:todos props)
(let [t (atom (sorted-map))]
(dotimes [x 5]
(add-todo t (str "Some todo " x)))
t))
filt (atom :all)
editing (atom nil)]
(fn []
(let [items (vals @todos)
done (->> items (filter :done) count)
active (- (count items) done)
pred (case @filt
:active (complement :done)
:done :done
:all identity)
curedit @editing]
(dbg "rendering main")
[:section#todoapp
[:header#header
[:h1 "todos"]
[todo-input {:id "new-todo" :placeholder "What needs to be done?"
:on-save (partial add-todo todos)}]]
[:section#main
[:input#toggle-all {:type "checkbox" :checked (zero? active)
:on-change (partial complete-all todos
(pos? active))}]
[:label {:for "toggle-all"} "Mark all as complete"]
[:ul#todo-list
(for [{id :id :as todo} (filter pred items)]
[todo-item {:key id :todo todo :editing editing
:is-editing (= curedit id)
:on-save (partial save todos id)
:on-toggle (partial toggle todos id)
:on-destroy (partial delete todos id)}])]]
[:footer#footer
[todo-stats {:active active :done done :filter filt
:clear (partial clear-done todos)}]]
[:footer#info
[:p "Double-click to edit a todo"]]]))))
(defn todo-app []
[todo-main])
(defn ^:export run []
(cloact/render-component [todo-app] (.-body js/document)))

View File

@ -0,0 +1,15 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>todomvc with cloact</title>
<link rel="stylesheet" href="todos.css">
</head>
<body>
<h1>This will become todomvc when the ClojureScript is compiled</h1>
<script type="text/javascript" src="target/client.js"></script>
<script type="text/javascript">
todomvc.run();
</script>
</body>
</html>

557
examples/todomvc/todos.css Normal file
View File

@ -0,0 +1,557 @@
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;
-ms-appearance: none;
-o-appearance: none;
appearance: none;
}
body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #eaeaea;
/* 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;
}
button,
input[type="checkbox"] {
outline: none;
}
#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: 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);
-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: -42px;
left: -4px;
width: 40px;
text-align: center;
/* Mobile Safari */
border: none;
}
#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;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
/* Mobile Safari */
border: none;
-webkit-appearance: none;
-ms-appearance: none;
-o-appearance: none;
appearance: none;
}
#todo-list li .toggle:after {
content: '✔';
/* 40 + a couple of pixels visual adjustment */
line-height: 43px;
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 {
white-space: pre;
word-break: break-word;
padding: 15px 60px 15px 15px;
margin-left: 45px;
display: block;
line-height: 1.2;
-webkit-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;
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);
-ms-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;
}
#todo-list li .toggle {
height: 40px;
}
#toggle-all {
top: -56px;
left: -15px;
width: 65px;
height: 41px;
-webkit-transform: rotate(90deg);
-ms-transform: rotate(90deg);
transform: rotate(90deg);
-webkit-appearance: none;
appearance: none;
}
}
.hidden {
display: none;
}
hr {
margin: 20px 0;
border: 0;
border-top: 1px dashed #C5C5C5;
border-bottom: 1px dashed #F7F7F7;
}
.learn a {
font-weight: normal;
text-decoration: none;
color: #b83f45;
}
.learn a:hover {
text-decoration: underline;
color: #787e7e;
}
.learn h3,
.learn h4,
.learn h5 {
margin: 10px 0;
font-weight: 500;
line-height: 1.2;
color: #000;
}
.learn h3 {
font-size: 24px;
}
.learn h4 {
font-size: 18px;
}
.learn h5 {
margin-bottom: 0;
font-size: 14px;
}
.learn ul {
padding: 0;
margin: 0 0 30px 25px;
}
.learn li {
line-height: 20px;
}
.learn p {
font-size: 15px;
font-weight: 300;
line-height: 1.3;
margin-top: 0;
margin-bottom: 0;
}
.quote {
border: none;
margin: 20px 0 60px 0;
}
.quote p {
font-style: italic;
}
.quote p:before {
content: '“';
font-size: 50px;
opacity: .15;
position: absolute;
top: -20px;
left: 3px;
}
.quote p:after {
content: '”';
font-size: 50px;
opacity: .15;
position: absolute;
bottom: -42px;
right: 3px;
}
.quote footer {
position: absolute;
bottom: -40px;
right: 0;
}
.quote footer img {
border-radius: 3px;
}
.quote footer a {
margin-left: 5px;
vertical-align: middle;
}
.speech-bubble {
position: relative;
padding: 10px;
background: rgba(0, 0, 0, .04);
border-radius: 5px;
}
.speech-bubble:after {
content: '';
position: absolute;
top: 100%;
right: 30px;
border: 13px solid transparent;
border-top-color: rgba(0, 0, 0, .04);
}
.learn-bar > .learn {
position: absolute;
width: 272px;
top: 8px;
left: -300px;
padding: 10px;
border-radius: 5px;
background-color: rgba(255, 255, 255, .6);
-webkit-transition-property: left;
transition-property: left;
-webkit-transition-duration: 500ms;
transition-duration: 500ms;
}
@media (min-width: 899px) {
.learn-bar {
width: auto;
margin: 0 0 0 300px;
}
.learn-bar > .learn {
left: 8px;
}
.learn-bar #todoapp {
width: 550px;
margin: 130px auto 40px auto;
}
}

37
project.clj Normal file
View File

@ -0,0 +1,37 @@
(defproject cloact "0.0.1-SNAPSHOT"
:dependencies [[org.clojure/clojurescript "0.0-2120"]]
:plugins [[lein-cljsbuild "1.0.1"]]
:hooks [leiningen.cljsbuild]
:profiles {:prod {:cljsbuild
{:builds
{:client {:compiler
{:optimizations :advanced
:pretty-print true}}}}}
:test {:plugins [[com.cemerick/clojurescript.test "0.2.1"]]
:cljsbuild
{:builds
{:client {:source-paths ["test"
"examples/todomvc/src"
"examples/simple/src"]}
:server {:source-paths ["test"]}}}}
:srcmap {:cljsbuild
{:builds
{:client
{:compiler
{:source-map "target/cljs-client.js.map"
:source-map-path "client"}}}}}}
:source-paths ["src"]
:cljsbuild
{:builds
{:server {:source-paths ["src"]
:compiler
{:output-dir "target/server"
:output-to "target/cljs-server.js"
:pretty-print true}}
:client {:source-paths ["src"]
:compiler
{:output-dir "target/client"
:output-to "target/cljs-client.js"
:pretty-print true}}}})

15
site/test.html Normal file
View File

@ -0,0 +1,15 @@
<html>
<head>
<meta charset="utf-8">
<title>Testing cloact</title>
<link rel="stylesheet" href="../examples/todomvc/todos.css">
<link rel="stylesheet" href="../examples/simple/example.css">
</head>
<body>
<h1>This will become an example when compiled</h1>
<script type="text/javascript" src="../target/cljs-client.js"></script>
<script type="text/javascript">
runtests.run();
</script>
</body>
</html>

73
src/cloact/core.cljs Normal file
View File

@ -0,0 +1,73 @@
(ns cloact.core
(:refer-clojure :exclude [partial atom])
(:require-macros [cloact.debug :refer [dbg prn]])
(:require [cloact.impl.template :as tmpl]
[cloact.impl.component :as comp]
[cloact.impl.util :as util]
[cloact.ratom :as ratom]))
(def React tmpl/React)
(defn create-class [body]
(comp/create-class body))
(defn as-component [comp]
(tmpl/as-component comp))
(defn render-component
([comp container]
(render-component comp container nil))
([comp container callback]
(.renderComponent React (as-component comp) container callback)))
(defn unmount-component-at-node [container]
(.unmountComponentAtNode React container))
(defn render-component-to-string [component callback]
(.renderComponentToString React (as-component component) callback))
(defn set-props [C props]
(comp/set-props C props))
(defn replace-props [C props]
(comp/replace-props C props))
(defn merge-props [defaults props]
(util/merge-props defaults props))
;; Ratom
(defn atom
"Like clojure.core/atom, except that it keeps track of derefs."
([x] (ratom/atom x))
([x & rest] (apply ratom/atom x rest)))
;; Utilities
(deftype partial-ifn [f args ^:mutable p]
IFn
(-invoke [_ & a]
(or p (set! p (apply clojure.core/partial f args)))
(apply p a))
IEquiv
(-equiv [_ other]
(and (= f (.-f other)) (= args (.-args other))))
IHash
(-hash [_] (hash [f args])))
(defn partial
"Works just like clojure.core/partial, except that it is an IFn, and
the result can be compared with ="
[f & args]
(partial-ifn. f args nil))
(let [p1 (partial vector 1 2)]
(assert (= (p1 3) [1 2 3]))
(assert (= p1 (partial vector 1 2)))
(assert (ifn? p1))
(assert (= (partial vector 1 2) p1))
(assert (not= p1 (partial vector 1 3))))

35
src/cloact/debug.clj Normal file
View File

@ -0,0 +1,35 @@
(ns cloact.debug
(:refer-clojure :exclude [prn println]))
(defmacro log
"Print with console.log, if it exists."
[& forms]
`(when (not (nil? (.-log js/console)))
(.log js/console ~@forms)))
(defmacro println
"Print string with console.log"
[& forms]
`(log (str ~@forms)))
(defmacro prn
"Like standard prn, but prints using console.log (so that we get
nice clickable links to source in modern browsers)."
[& forms]
`(log (pr-str ~@forms)))
(defmacro dbg
"Useful debugging macro that prints the source and value of x,
as well as package name and line number. Returns x."
[x]
(let [ns (str cljs.analyzer/*cljs-ns*)]
`(let [x# ~x]
(println (str "dbg "
~ns ":"
~(:line (meta &form))
": "
~(pr-str x)
": "
(pr-str x#)))
x#)))

3
src/cloact/debug.cljs Normal file
View File

@ -0,0 +1,3 @@
(ns cloact.debug)
;; Empty file, to allow require with :refer-macros

View File

@ -0,0 +1,231 @@
(ns cloact.impl.component
(:require-macros [cloact.ratom :refer [reaction]]
[cloact.debug :refer [dbg prn]])
(:require [cloact.impl.template :as tmpl]
[cloact.impl.util :as util]
[cloact.ratom :as ratom]))
(def React tmpl/React)
;;; Atom protocol as mixin
(def CloactMixin (js-obj))
(def -ToExtend (js-obj))
(set! (.-prototype -ToExtend) CloactMixin)
(declare get-props)
(declare get-children)
(extend-type -ToExtend
IEquiv
(-equiv [C other] (identical? C other))
IDeref
(-deref [C] (.-state C))
IMeta
(-meta [C] nil)
IPrintWithWriter
(-pr-writer [C writer opts]
(-write writer (str "#<" (-> C .-constructor .-displayName) ": "))
(pr-writer (.-state C) writer opts)
(-write writer ">"))
IWatchable
(-notify-watches [C old new]
(.replaceState C new))
(-add-watch [C key f] (assert false "Component isn't really watchable"))
(-remove-watch [C key] (assert false "Component isn't really watchable"))
ILookup
(-lookup [C key]
(-lookup C key nil))
(-lookup [C key not-found]
(case key
:props (get-props C)
:children (get-children C)
:dom-node (.getDOMNode C)
:refs (.-refs C)
not-found))
IHash
(-hash [C] (goog/getUid C)))
(doseq [x (js-keys CloactMixin)]
;; Tell React to not autobind
(aset (aget CloactMixin x) "__reactDontBind" true))
;; Reference -ToExtend to show fucking google closure that it is used
(when-not -ToExtend
(.log js/console "this should never happen to " -ToExtend))
;;; Function wrapping
(defn- args-of [C]
(-> C .-props .-cljsArgs))
(defn- cljs-props [C]
(let [args (args-of C)
p (nth args 1 nil)]
(when (map? p)
p)))
(defn- first-child [args]
(let [p? (nth args 1 nil)]
(if (or (nil? p?) (map? p?)) 2 1)))
(defn- get-children [C]
(let [args (args-of C)
c (first-child args)]
(drop c args)))
(defn replace-props [C newprops]
(let [obj (js-obj)]
(set! (.-cljsArgs obj)
(apply vector
(nth (args-of C) 0)
newprops
(get-children C)))
(.setProps C obj)))
(defn set-props [C newprops]
(replace-props C (merge (cljs-props C) newprops)))
(defn get-props [C]
(let [ctx ratom/*ratom-context*]
(if (or (nil? ctx) (.-isRenderContext ctx))
(cljs-props C)
;; Use atom if getting props in an ratom
(deref (or (.-cljsPropsAtom C)
(set! (.-cljsPropsAtom C) (ratom/ratom (cljs-props C))))))))
(defn- do-render [C f]
(set! (.-isRenderContext ratom/*ratom-context*) true)
(let [res (f (cljs-props C) f @C)
conv (if (vector? res)
(tmpl/as-component res)
(if (fn? res)
(do-render C (set! (.-cljsRenderFn C) res))
res))]
conv))
(defn- render [C]
(assert C)
(when (nil? (.-cljsRatom C))
(set! (.-cljsRatom C)
(reaction :auto-run #(.forceUpdate C)
(do-render C (.-cljsRenderFn C)))))
(ratom/run (.-cljsRatom C)))
(defn- custom-wrapper [key f]
(case key
:getDefaultProps
(assert false "getDefaultProps not supported yet")
:getInitialState
(fn [C]
;; reset! doesn't call -notifyWatches unless -watches is set
(set! (.-watches C) {})
(when f
(set! (.-cljsOldState C)
(merge (.-state C) (f C)))))
:componentWillReceiveProps
(fn [C props]
(when-not (nil? (.-cljsPropsAtom C))
(reset! (.-cljsPropsAtom C) (cljs-props C)))
(when f (f C props)))
:shouldComponentUpdate
(fn [C nextprops nextstate]
(assert (nil? f) "shouldComponentUpdate is not yet supported")
(assert (vector? (-> C .-props .-cljsArgs)))
(let [a1 (-> C .-props .-cljsArgs)
a2 (-> nextprops .-cljsArgs)
ostate (.-cljsOldState C)
eq (and (tmpl/equal-args a1 a2)
(= ostate nextstate))]
(set! (.-cljsOldState C) nextstate)
(not eq)))
:componentWillUnmount
(fn [C]
(ratom/dispose! (.-cljsRatom C))
(when f (f C)))
:render
(fn [C]
(if (nil? (.-cljsRenderFn C))
(set! (.-cljsRenderFn C) f))
(render C))
nil))
(defn- default-wrapper [f]
(if (fn? f)
(fn [& args]
(this-as C (apply f C args)))
f))
(defn- get-wrapper [key f name]
;; (when (and name (fn? f) (nil? (aget f "displayName")))
;; (aset f "displayName" (str name key)))
(let [wrap (custom-wrapper key f)]
(when (and wrap f)
(assert (fn? f)
(str "Expected function in " name key " but got " f)))
(default-wrapper (or wrap f))))
(def obligatory {:getInitialState nil
:componentWillReceiveProps nil
:shouldComponentUpdate nil
:componentWillUnmount nil})
(def aliases {:initialState :getInitialState
:defaultProps :getDefaultProps})
(defn- camelify-map-keys [m]
(into {} (for [[k v] m]
[(-> k tmpl/dash-to-camel keyword) v])))
(defn- allow-aliases [m]
(into {} (for [[k v] m]
[(get aliases k k) v])))
(defn- add-obligatory [fun-map]
(merge obligatory fun-map))
(defn- wrap-funs [fun-map]
(let [name (or (:displayName fun-map)
(when-let [r (:render fun-map)]
(or (.-displayName r)
(.-name r))))
name1 (if (empty? name) (str (gensym "cloact")) name)]
(into {} (for [[k v] (assoc fun-map :displayName name1)]
[k (get-wrapper k v name)]))))
(defn- add-atom-mixin
[props-map]
(merge-with concat props-map {:mixins [CloactMixin]}))
(defn- cljsify [body]
(-> body
camelify-map-keys
allow-aliases
add-obligatory
wrap-funs
add-atom-mixin
clj->js))
(defn create-class
[body]
(let [spec (cljsify body)
res (.createClass React spec)
f (fn [& args]
(let [arg (js-obj)]
(set! (.-cljsArgs arg) (apply vector res args))
(res arg)))]
(set! (.-cljsReactClass f) res)
(set! (.-cljsReactClass res) res)
f))

View File

@ -0,0 +1,158 @@
(ns cloact.impl.template
(:require-macros [cloact.debug :refer [dbg prn println]])
(:require [clojure.string :as string]
[cloact.react :as reacts]))
(def React reacts/React)
(defn dash-to-camel [dashed]
(let [words (string/split (name dashed) #"-")
camels (map string/capitalize (rest words))]
(apply str (first words) camels)))
;; From Weavejester's Hiccup, via pump:
;; https://github.com/weavejester/hiccup/blob/master/src/hiccup/compiler.clj#L32
(def ^{:doc "Regular expression that parses a CSS-style id and class
from a tag name."
:private true}
re-tag #"([^\s\.#]+)(?:#([^\s\.#]+))?(?:\.([^\s#]+))?")
(defn parse-tag [tag]
(let [[tag id class] (->> tag name (re-matches re-tag) next)
comp (aget (.-DOM React) tag)
class' (when class
(string/replace class #"\." " "))]
[comp (when (or id class')
[id class'])]))
(def attr-aliases {"class" "className"
"for" "htmlFor"})
(defn undash-prop-name [n]
(let [undashed (dash-to-camel n)]
(get attr-aliases undashed undashed)))
(def cached-prop-name (memoize undash-prop-name))
(def cached-style-name (memoize dash-to-camel))
(defn convert-prop-value [val]
(cond (map? val) (let [obj (js-obj)]
(doseq [[k v] val]
(aset obj (cached-style-name k) (clj->js v)))
obj)
(ifn? val) (fn [& args] (apply val args))
:else (clj->js val)))
(defn set-tag-extra [props [id class]]
(set! (.-id props) id)
(when class
(set! (.-className props)
(if-let [old (.-className props)]
(str class " " old)
class))))
(defn convert-props [props extra]
(let [is-empty (empty? props)]
(cond
(and is-empty (nil? extra)) nil
(identical? (type props) js/Object) props
:else (let [objprops (js-obj)]
(when-not is-empty
(doseq [[k v] props]
(aset objprops (cached-prop-name k)
(convert-prop-value v))))
(when-not (nil? extra)
(set-tag-extra objprops extra))
objprops))))
(defn identical-parts [v1 v2 from]
(let [end (count v1)]
(loop [n from]
(if (>= n end)
true
(if (identical? (nth v1 n) (nth v2 n))
(recur (inc n))
false)))))
(defn equal-args [v1 v2]
(let [c1 (count v1)]
(and (= (nth v1 0) (nth v2 0))
(identical? c1 (count v2))
(if (< c1 2)
true
(let [props1 (nth v1 1)]
(if (or (nil? props1) (map? props1))
(and (= props1 (nth v2 1))
(identical-parts v1 v2 2))
(identical-parts v1 v2 1)))))))
(declare wrapper)
(defn fn-to-class [f]
(let [spec (meta f)
withrender (merge spec {:render f})
res (cloact.core/create-class withrender)]
(set! (.-cljsReactClass f) (.-cljsReactClass res))
res))
(defn as-class [x]
(cond
(keyword? x) wrapper
(not (nil? (.-cljsReactClass x))) x
:else (do (assert (fn? x))
(if (.isValidClass React x)
wrapper
(fn-to-class x)))))
(defn vec-to-comp [x]
(let [[tag p] x
c (.-cljsReactClass (as-class tag))
a (js-obj)]
(set! (.-cljsArgs a) x)
(when (map? p)
(set! (.-key a) (:key p))
(set! (.-ref a) (:ref p)))
(c a)))
(defn map-into-array [f coll]
(let [a (into-array coll)
len (alength a)]
(dotimes [i len]
(aset a i (f (aget a i))))
a))
(defn as-component [x]
(cond (vector? x) (vec-to-comp x)
(seq? x) (map-into-array as-component x)
true x))
(def cached-tag (memoize parse-tag))
(defn render-wrapped [this]
(let [inprops (.-props this)
args (.-cljsArgs inprops)
[tag scnd] args
hasprops (or (nil? scnd) (map? scnd))
[native extra] (when (keyword? tag) (cached-tag tag))
f (or native tag)
jsprops (convert-props (when hasprops scnd) extra)
jsargs (->> args
(drop (if hasprops 2 1))
(map-into-array as-component))]
(assert (.isValidClass React f))
(assert (nil? (.-cljsReactClass f)))
(.apply f nil (.concat (array jsprops) jsargs))))
(defn should-update-wrapped [C nextprops nextstate]
(let [a1 (-> C .-props .-cljsArgs)
a2 (-> nextprops .-cljsArgs)]
(not (equal-args a1 a2))))
(def wrapper
(.createClass React (js-obj "render"
#(this-as C (render-wrapped C))
"shouldComponentUpdate"
#(this-as C (should-update-wrapped C %1 %2)))))
(set! (.-cljsReactClass wrapper) wrapper)

28
src/cloact/impl/util.cljs Normal file
View File

@ -0,0 +1,28 @@
(ns cloact.impl.util)
(defn- merge-class [p1 p2]
(let [class (when-let [c1 (:class p1)]
(when-let [c2 (:class p2)]
(str c1 " " c2)))]
(if (nil? class)
p2
(assoc p2 :class class))))
(defn- merge-style [p1 p2]
(let [style (when-let [s1 (:style p1)]
(when-let [s2 (:style p2)]
(merge s1 s2)))]
(if (nil? style)
p2
(assoc p2 :style style))))
(defn merge-props [p1 p2]
(if (nil? p1)
p2
(do
(when-not (map? p1)
(.log js/console p1))
(assert (map? p1))
(merge-style p1 (merge-class p1 (merge p1 p2))))))

20
src/cloact/ratom.clj Normal file
View File

@ -0,0 +1,20 @@
(ns cloact.ratom)
(defn extract-opts [forms]
(let [opts (->> forms
(partition 2)
(take-while #(keyword? (first %)))
(apply concat))]
[opts (drop (count opts) forms)]))
(defmacro reaction [& body]
(let [[opts# main#] (extract-opts body)]
`(cloact.ratom/make-reaction
(fn [] ~@main#) ~@opts#)))
(defmacro run!
"Runs body immediately, and runs again whenever atoms deferenced in the body change. Body should side effect."
[& body]
`(let [co# (reaction :auto-run true ~@body)]
(deref co#)
co#))

163
src/cloact/ratom.cljs Normal file
View File

@ -0,0 +1,163 @@
(ns cloact.ratom
(:refer-clojure :exclude [atom])
(:require-macros [cloact.debug :refer (dbg)]))
(declare ^:dynamic *ratom-context*)
(def -running (clojure.core/atom 0))
(defn running [] @-running)
(defn- capture-derefed [f]
(binding [*ratom-context* (clojure.core/atom #{})]
[(f) @*ratom-context*]))
(defn- notify-deref-watcher! [derefable]
(when-not (or (nil? *ratom-context*))
(swap! *ratom-context* conj derefable)))
;; Have atoms make a note when they're dereferenced.
;; (extend-type Atom
;; IDeref
;; (-deref [this]
;; (notify-deref-watcher! this)
;; (.-state this)))
(deftype RAtom [state meta validator watches]
IEquiv
(-equiv [o other] (identical? o other))
IDeref
(-deref [this]
(notify-deref-watcher! this)
state)
IMeta
(-meta [_] meta)
IPrintWithWriter
(-pr-writer [a writer opts]
(-write writer "#<Atom: ")
(pr-writer state writer opts)
(-write writer ">"))
IWatchable
(-notify-watches [this oldval newval]
(doseq [[key f] watches]
(f key this oldval newval)))
(-add-watch [this key f]
(set! (.-watches this) (assoc watches key f)))
(-remove-watch [this key]
(set! (.-watches this) (dissoc watches key)))
IHash
(-hash [this] (goog/getUid this)))
(defn atom
"Like clojure.core/atom, except that it keeps track of derefs."
([x] (RAtom. x nil nil nil))
([x & {:keys [meta validator]}] (RAtom. x meta validator nil)))
(defprotocol IDisposable
(dispose! [this]))
(defprotocol IRunnable
(run [this]))
(defprotocol IComputedImpl
(-update-watching [this derefed])
(-handle-change [k sender oldval newval]))
(defn- call-watches [obs watches oldval newval]
(doseq [[k wf] watches]
(wf k obs oldval newval)))
(deftype Reaction [f ^:mutable state ^:mutable dirty? ^:mutable active?
^:mutable watching ^:mutable watches
auto-run on-set on-dispose]
IWatchable
(-notify-watches [this oldval newval]
(when on-set
(on-set oldval newval))
(call-watches this watches oldval newval))
(-add-watch [this k wf]
(set! watches (assoc watches k wf)))
(-remove-watch [this k]
(set! watches (dissoc watches k))
(when (empty? watches)
(dispose! this)))
IComputedImpl
(-handle-change [this sender oldval newval]
(when (and active? (not dirty?) (not (identical? oldval newval)))
(set! dirty? true)
((or auto-run run) this)))
(-update-watching [this derefed]
(doseq [w derefed]
(when-not (contains? watching w)
(add-watch w this -handle-change)))
(doseq [w watching]
(when-not (contains? derefed w)
(remove-watch w this)))
(set! watching derefed))
IRunnable
(run [this]
(let [oldstate state
[res derefed] (capture-derefed f)]
(when (not= derefed watching)
(-update-watching this derefed))
(when-not active?
(swap! -running inc)
(set! active? true))
(set! dirty? false)
(set! state res)
(call-watches this watches oldstate state)
res))
IDeref
(-deref [this]
;; TODO: relax this?
(when (not (or auto-run *ratom-context*))
(dbg [auto-run *ratom-context*]))
(assert (or auto-run *ratom-context*)
"Reaction derefed outside auto-running context")
(notify-deref-watcher! this)
(if dirty?
(run this)
state))
IDisposable
(dispose! [this]
(doseq [w watching]
(remove-watch w this))
(set! watching #{})
(set! state nil)
(set! dirty? true)
(when active?
(swap! -running dec)
(set! active? false))
(when on-dispose
(on-dispose)))
IEquiv
(-equiv [o other] (identical? o other))
IPrintWithWriter
(-pr-writer [this writer opts]
(-write writer (str "#<Reaction " (hash this) ": "))
(pr-writer state writer opts)
(-write writer ">"))
IHash
(-hash [this] (goog/getUid this)))
(defn make-reaction [f & {:keys [auto-run on-set on-dispose]}]
(let [runner (if (= auto-run true) run auto-run)]
(Reaction. f nil true false
#{} {}
runner on-set on-dispose)))

15811
src/cloact/react.cljs Normal file

File diff suppressed because it is too large Load Diff

54
test/runtests.cljs Normal file
View File

@ -0,0 +1,54 @@
(ns runtests
(:require-macros [cemerick.cljs.test
:refer (is deftest with-test run-tests testing)]
[cloact.debug :refer [dbg println]])
(:require [cemerick.cljs.test :as t]
[cloact.core :as cloact :refer [atom]]
[todomvc :as todomvc]))
(defn ^:export console-print [x]
(when (not= x "\n")
(println x)))
(set-print-fn! console-print)
(def test-results (atom nil))
(js/setTimeout
(fn []
(println "-----------------------------------------")
(reset! test-results (t/run-all-tests))
(println "-----------------------------------------"))
1000)
(defn test-output []
(let [res @test-results]
[:div
(if-not res
[:div "waiting for tests to run"]
[:div
[:p (str "Ran " (:test res) " tests containing "
(+ (:pass res) (:fail res) (:error res))
" assertions.")]
[:p (:fail res) " failues, " (:error res) " errors."]])]))
(defn examples []
(let [p {:style {:color "#aaa"}}]
[:div
[:div
[:h2 p "Test results:"]
[test-output]]
[:div
[:h2 p "Simple example:"]
[simpleexample/simple-example]]
[:div
[:h2 p "Todomvc:"]
[todomvc/todo-app]]]))
(defn test-main []
[examples])
(defn ^:export run []
(cloact/render-component [test-main]
(.-body js/document)))

18
test/simpletest.cljs Normal file
View File

@ -0,0 +1,18 @@
(ns simpletest
(:require-macros [cemerick.cljs.test
:refer (is deftest with-test run-tests testing)])
(:require [cemerick.cljs.test :as t]))
;; (deftest somewhat-less-wat
;; (is (= "{}[]" (+ {} []))))
(deftest javascript-allows-div0
(is (= js/Infinity (/ 1 0) (/ (int 1) (int 0)))))
;; (with-test
;; (defn pennies->dollar-string
;; [pennies]
;; {:pre [(integer? pennies)]}
;; (str "$" (int (/ pennies 100)) "." (mod pennies 100)))
;; (testing "assertions are nice"
;; (is (thrown-with-msg? js/Error #"integer?" (pennies->dollar-string 564.2)))))

121
test/testcloact.cljs Normal file
View File

@ -0,0 +1,121 @@
(ns testcloact
(:require-macros [cemerick.cljs.test
:refer (is deftest with-test run-tests testing)]
[cloact.ratom :refer [reaction]]
[cloact.debug :refer [dbg println]])
(:require [cemerick.cljs.test :as t]
[cloact.core :as r :refer [atom]]
[cloact.ratom :as rv]))
(defn running [] (rv/running))
(def isClient (not (nil? (try (.-document js/window)
(catch js/Object e nil)))))
(defn add-test-div [name]
(let [doc js/document
body (.-body js/document)
div (.createElement doc "div")]
(.appendChild body div)
div))
(defn with-mounted-component [comp f]
(when isClient
(let [div (add-test-div "_testcloact")]
(let [comp (r/render-component comp div #(f comp div))]
(r/unmount-component-at-node div)))))
(defn found-in [re div]
(re-find re (.-innerHTML div)))
(def tests-run (clojure.core/atom 0))
(def tests-should-run (clojure.core/atom 0))
(defn really-simple []
[:div "div in really-simple"])
(deftest really-simple-test
(swap! tests-should-run inc)
(with-mounted-component [really-simple nil nil]
(fn [c div]
(swap! tests-run inc)
(is (found-in #"div in really-simple" div)))))
(deftest test-simple-callback
(swap! tests-should-run + 6)
(let [comp (r/create-class
{:component-did-mount #(swap! tests-run inc)
:render (fn [P C S]
(assert (map? P))
(swap! tests-run inc)
[:div (str "hi " (:foo P) ".")])})]
(with-mounted-component (comp {:foo "you"})
(fn [C div]
(swap! tests-run inc)
(is (found-in #"hi you" div))
(r/set-props C {:foo "there"})
(is (found-in #"hi there" div))
(let [runs @tests-run]
(r/set-props C {:foo "there"})
(is (found-in #"hi there" div))
(is (= runs @tests-run)))
(r/replace-props C {:foobar "not used"})
(is (found-in #"hi ." div))))))
(deftest test-state-change
(swap! tests-should-run + 3)
(let [comp (r/create-class
{:get-initial-state (fn [])
:render (fn [P C S]
(swap! tests-run inc)
[:div (str "hi " (:foo S))])})]
(with-mounted-component (comp)
(fn [C div]
(swap! tests-run inc)
(is (found-in #"hi " div))
(swap! C assoc :foo "there")
(is (found-in #"hi there" div))
(let [runs @tests-run]
;; should not be rendered
(swap! C assoc :foo "there")
(is (found-in #"hi there" div))
(is (= runs @tests-run)))
(swap! C assoc :foo "you")
(is (found-in #"hi you" div))))))
(deftest test-ratom-change
(swap! tests-should-run + 3)
(let [runs (running)
val (atom 0)
v1 (reaction @val)
ran @tests-run
comp (fn []
(swap! tests-run inc)
[:div (str "val " @v1)])]
(with-mounted-component [comp]
(fn [C div]
(swap! tests-run inc)
(is (not= runs (running)))
(is (found-in #"val 0" div))
(is (= @tests-run (+ ran 2)))
(reset! val 1)
(is (found-in #"val 1" div))
(is (= @tests-run (+ ran 3)))
;; should not be rendered
(reset! val 1)
(is (found-in #"val 1" div))
(is (= @tests-run (+ ran 3)))))
(is (= runs (running)))))
(deftest check-that-test-ran
(if isClient
(is (= @tests-run @tests-should-run))
(is (= @tests-run 0))))

226
test/testratom.cljs Normal file
View File

@ -0,0 +1,226 @@
(ns testratom
(:require-macros [cemerick.cljs.test
:refer (is deftest with-test run-tests testing)]
[cloact.ratom :refer [run! reaction]]
[cloact.debug :refer [dbg]])
(:require [cemerick.cljs.test :as t]
[cloact.ratom :as rv]))
(defn running [] (rv/running))
(defn dispose [v] (rv/dispose! v))
(defn ratom-perf []
(dbg "ratom-perf")
(let [a (rv/atom 0)
mid (reaction (inc @a))
res (run!
(inc @mid))]
(time (dotimes [x 100000]
(swap! a inc)))
(dispose res)))
;; (ratom-perf)
(deftest basic-ratom
(let [runs (running)
start (rv/atom 0)
sv (reaction @start)
comp (reaction @sv (+ 2 @sv))
c2 (reaction (inc @comp))
count (rv/atom 0)
out (rv/atom 0)
res (reaction
(swap! count inc)
@sv @c2 @comp)
const (run!
(reset! out @res))]
(is (= @count 1) "constrain ran")
(is (= @out 2))
(reset! start 1)
(is (= @out 3))
(is (= @count 4))
(dispose const)
(is (= (running) runs))))
(deftest double-dependency
(let [runs (running)
start (rv/atom 0)
c3-count (rv/atom 0)
c1 (reaction @start 1)
c2 (reaction @start)
c3 (rv/make-reaction
(fn []
(swap! c3-count inc)
(+ @c1 @c2))
:auto-run true)]
(is (= @c3-count 0))
(is (= @c3 1))
(is (= @c3-count 1) "t1")
(swap! start inc)
(is (= @c3-count 2) "t2")
(is (= @c3 2))
(is (= @c3-count 2) "t3")
(dispose c3)
(is (= (running) runs))))
(deftest test-from-reflex
(let [runs (running)]
(let [!counter (rv/atom 0)
!signal (rv/atom "All I do is change")
co (run!
;;when I change...
@!signal
;;update the counter
(swap! !counter inc))]
(is (= 1 @!counter) "Constraint run on init")
(reset! !signal "foo")
(is (= 2 @!counter)
"Counter auto updated")
(dispose co))
(let [!x (rv/atom 0)
!co (reaction :auto-run true (inc @!x))]
(is (= 1 @!co) "CO has correct value on first deref")
(swap! !x inc)
(is (= 2 @!co) "CO auto-updates")
(dispose !co))
(is (= runs (running)))))
(deftest test-unsubscribe
(dotimes [x 10]
(let [runs (running)
a (rv/atom 0)
a1 (reaction (inc @a))
a2 (reaction @a)
b-changed (rv/atom 0)
c-changed (rv/atom 0)
b (reaction
(swap! b-changed inc)
(inc @a1))
c (reaction
(swap! c-changed inc)
(+ 10 @a2))
res (run!
(if (< @a2 1) @b @c))]
(is (= @res (+ 2 @a)))
(is (= @b-changed 1))
(is (= @c-changed 0))
(reset! a -1)
(is (= @res (+ 2 @a)))
(is (= @b-changed 2))
(is (= @c-changed 0))
(reset! a 2)
(is (= @res (+ 10 @a)))
(is (<= 2 @b-changed 3))
(is (= @c-changed 1))
(reset! a 3)
(is (= @res (+ 10 @a)))
(is (<= 2 @b-changed 3))
(is (= @c-changed 2))
(reset! a 3)
(is (= @res (+ 10 @a)))
(is (<= 2 @b-changed 3))
(is (= @c-changed 2))
(reset! a -1)
(is (= @res (+ 2 @a)))
(dispose res)
(is (= runs (running))))))
(deftest maybe-broken
(let [runs (running)]
(let [runs (running)
a (rv/atom 0)
b (reaction (inc @a))
c (reaction (dec @a))
d (reaction (str @b))
res (rv/atom 0)
cs (run!
(reset! res @d))]
(is (= @res "1"))
(dispose cs))
;; should be broken according to https://github.com/lynaghk/reflex/issues/1
;; but isnt
(let [a (rv/atom 0)
b (reaction (inc @a))
c (reaction (dec @a))
d (run! [@b @c])]
(is (= @d [1 -1]))
(dispose d))
(let [a (rv/atom 0)
b (reaction (inc @a))
c (reaction (dec @a))
d (run! [@b @c])
res (rv/atom 0)]
(is (= @d [1 -1]))
(let [e (run! (reset! res @d))]
(is (= @res [1 -1]))
(dispose e))
(dispose d))
(is (= runs (running)))))
(deftest test-dispose
(dotimes [x 10]
(let [runs (running)
a (rv/atom 0)
disposed (rv/atom nil)
disposed-c (rv/atom nil)
disposed-cns (rv/atom nil)
count-b (rv/atom 0)
b (reaction
:on-dispose #(reset! disposed true)
(swap! count-b inc)
(inc @a))
c (reaction
:on-dispose #(reset! disposed-c true)
(if (< @a 1) (inc @b) (dec @a)))
res (rv/atom nil)
cns (run!
:on-dispose #(reset! disposed-cns true)
(reset! res @c))]
(is (= @res 2))
(is (= (+ 3 runs) (running)))
(is (= @count-b 1))
(reset! a -1)
(is (= @res 1))
(is (= @disposed nil))
(is (= @count-b 2))
(is (= (+ 3 runs) (running)) "still running")
(reset! a 2)
(is (= @res 1))
(is (= @disposed true))
(is (= (+ 2 runs) (running)) "less running count")
(reset! disposed nil)
(reset! a -1)
;; This fails sometimes on node. I have no idea why.
(is (= 1 @res) "should be one again")
(is (= @disposed nil))
(reset! a 2)
(is (= @res 1))
(is (= @disposed true))
(dispose cns)
(is (= @disposed-c true))
(is (= @disposed-cns true))
(is (= runs (running))))))
(deftest test-on-set
(let [runs (running)
a (rv/atom 0)
b (run!
:on-set (fn [oldv newv]
(reset! a (+ 10 newv)))
(+ 5 @a))]
(is (= 5 @b))
(reset! a 1)
(is (= 6 @b))
(reset! b 1)
(is (= 11 @a))
(is (= 16 @b))
(dispose b)
(is (= runs (running)))))