mirror of
https://github.com/status-im/reagent.git
synced 2025-02-24 09:38:21 +00:00
Initial version
This commit is contained in:
commit
12566ced7b
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
target
|
||||
pom.xml
|
||||
.lein-repl-history
|
63
Makefile
Normal file
63
Makefile
Normal 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
114
bin/gencljs.js
Normal 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
62
bin/gencljs.sh
Executable 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
16
bower_components/react/.bower.json
vendored
Normal 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
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
5
bower_components/react/bower.json
vendored
Normal 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
15332
bower_components/react/react-with-addons.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
21
bower_components/react/react-with-addons.min.js
vendored
Normal file
21
bower_components/react/react-with-addons.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
14294
bower_components/react/react.js
vendored
Normal file
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
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
20
examples/simple/Makefile
Normal 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
|
15
examples/simple/example.css
Normal file
15
examples/simple/example.css
Normal 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;
|
||||
}
|
15
examples/simple/example.html
Normal file
15
examples/simple/example.html
Normal 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>
|
25
examples/simple/project.clj
Normal file
25
examples/simple/project.clj
Normal 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}}}})
|
38
examples/simple/src/simpleexample.cljs
Normal file
38
examples/simple/src/simpleexample.cljs
Normal 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
20
examples/todomvc/Makefile
Normal 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
|
25
examples/todomvc/project.clj
Normal file
25
examples/todomvc/project.clj
Normal 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}}}})
|
115
examples/todomvc/src/todomvc.cljs
Normal file
115
examples/todomvc/src/todomvc.cljs
Normal 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)))
|
15
examples/todomvc/todomvc.html
Normal file
15
examples/todomvc/todomvc.html
Normal 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
557
examples/todomvc/todos.css
Normal 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
37
project.clj
Normal 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
15
site/test.html
Normal 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
73
src/cloact/core.cljs
Normal 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
35
src/cloact/debug.clj
Normal 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
3
src/cloact/debug.cljs
Normal file
@ -0,0 +1,3 @@
|
||||
(ns cloact.debug)
|
||||
|
||||
;; Empty file, to allow require with :refer-macros
|
231
src/cloact/impl/component.cljs
Normal file
231
src/cloact/impl/component.cljs
Normal 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))
|
158
src/cloact/impl/template.cljs
Normal file
158
src/cloact/impl/template.cljs
Normal 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
28
src/cloact/impl/util.cljs
Normal 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
20
src/cloact/ratom.clj
Normal 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
163
src/cloact/ratom.cljs
Normal 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
15811
src/cloact/react.cljs
Normal file
File diff suppressed because it is too large
Load Diff
54
test/runtests.cljs
Normal file
54
test/runtests.cljs
Normal 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
18
test/simpletest.cljs
Normal 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
121
test/testcloact.cljs
Normal 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
226
test/testratom.cljs
Normal 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)))))
|
||||
|
Loading…
x
Reference in New Issue
Block a user