112 |
113 |
114 |
115 |
116 | "))
117 |
118 |
119 | (defn -main [& args]
120 | (println "Writing \"index.html\"")
121 | (spit "index.html" page))
122 |
123 |
--------------------------------------------------------------------------------
/src/prum/core.clj:
--------------------------------------------------------------------------------
1 | (ns prum.core
2 | (:refer-clojure :exclude [ref])
3 | (:require
4 | [prum.compiler :as c]
5 | [prum.cursor :as cursor]
6 | [prum.server-render :as render]
7 | [prum.util :as util :refer [collect collect* call-all]]
8 | [prum.derived-atom :as derived-atom])
9 | (:import
10 | [prum.cursor Cursor]))
11 |
12 |
13 | (defn- fn-body? [form]
14 | (when (and (seq? form)
15 | (vector? (first form)))
16 | (if (= '< (second form))
17 | (throw (IllegalArgumentException. "Mixins must be given before argument list"))
18 | true)))
19 |
20 |
21 | (defn- parse-defc
22 | ":name :doc? :mixins* :bodies+
23 | symbol string < exprs fn-body?"
24 | [xs]
25 | (when-not (instance? clojure.lang.Symbol (first xs))
26 | (throw (IllegalArgumentException. "First argument to defc must be a symbol")))
27 | (loop [res {}
28 | xs xs
29 | mode nil]
30 | (let [x (first xs)
31 | next (next xs)]
32 | (cond
33 | (and (empty? res) (symbol? x))
34 | (recur {:name x} next nil)
35 | (fn-body? xs) (assoc res :bodies (list xs))
36 | (every? fn-body? xs) (assoc res :bodies xs)
37 | (string? x) (recur (assoc res :doc x) next nil)
38 | (= '< x) (recur res next :mixins)
39 | (= mode :mixins)
40 | (recur (update-in res [:mixins] (fnil conj []) x) next :mixins)
41 | :else
42 | (throw (IllegalArgumentException. (str "Syntax error at " xs)))))))
43 |
44 |
45 | (defn- compile-body [[argvec conditions & body]]
46 | (if (and (map? conditions) (seq body))
47 | (list argvec conditions (c/compile-html `(do ~@body)))
48 | (list argvec (c/compile-html `(do ~@(cons conditions body))))))
49 |
50 |
51 | (defn- -defc [builder cljs? body]
52 | (let [{:keys [name doc mixins bodies]} (parse-defc body)
53 | render-body (if cljs?
54 | (map compile-body bodies)
55 | bodies)
56 | arglists (if (= builder 'prum.core/build-defc)
57 | (map (fn [[arglist & _body]] arglist) bodies)
58 | (map (fn [[[_ & arglist] & _body]] (vec arglist)) bodies))]
59 | `(def ~(vary-meta name update :arglists #(or % `(quote ~arglists)))
60 | ~@(if doc [doc] [])
61 | (prum.core/lazy-component ~builder (fn ~@render-body) ~mixins ~(str name)))))
62 |
63 |
64 | (defn lazy-component [builder render mixins display-name]
65 | (builder render mixins display-name))
66 |
67 |
68 | (defmacro defc
69 | "Defc does couple of things:
70 |
71 | 1. Wraps body into prum.compiler/html
72 | 2. Generates render function from that
73 | 3. Takes render function and mixins, builds React class from them
74 | 4. Using that class, generates constructor fn [args]->ReactElement
75 | 5. Defines top-level var with provided name and assigns ctor to it
76 |
77 | (prum/defc label [t]
78 | [:div t])
79 |
80 | ;; creates React class
81 | ;; defines ctor fn (defn label [t] ...) => element
82 |
83 | (label \"text\") ;; => returns React element built with label class
84 |
85 | Usage:
86 |
87 | (defc name doc-string? [< mixins+]? [params*] render-body+)"
88 | [& body]
89 | (-defc 'prum.core/build-defc (boolean (:ns &env)) body))
90 |
91 |
92 | (defmacro defcs
93 | "Same as defc, but render will take additional first argument: state
94 |
95 | Usage:
96 |
97 | (defcs name doc-string? [< mixins+]? [state params*] render-body+)"
98 | [& body]
99 | (-defc 'prum.core/build-defcs (boolean (:ns &env)) body))
100 |
101 |
102 | (defmacro defcc
103 | "Same as defc, but render will take additional first argument: react component
104 |
105 | Usage:
106 |
107 | (defcc name doc-string? [< mixins+]? [comp params*] render-body+)"
108 | [& body]
109 | (-defc 'prum.core/build-defcc (boolean (:ns &env)) body))
110 |
111 |
112 | (defn- build-ctor [render mixins display-name]
113 | (let [init (collect :init mixins) ;; state props -> state
114 | will-mount (collect* [:will-mount ;; state -> state
115 | :before-render] mixins) ;; state -> state
116 | render render ;; state -> [dom state]
117 | wrapped-render (reduce #(%2 %1) render (collect :wrap-render mixins))] ;; render-fn -> render-fn
118 | (fn [& args]
119 | (let [props nil
120 | state (-> {:prum/args args}
121 | (call-all init props)
122 | (call-all will-mount))
123 | [dom _] (wrapped-render state)]
124 | (or dom [:prum/nothing])))))
125 |
126 |
127 | (defn build-defc [render-body mixins display-name]
128 | (if (empty? mixins)
129 | (fn [& args] (or (apply render-body args) [:prum/nothing]))
130 | (let [render (fn [state] [(apply render-body (:prum/args state)) state])]
131 | (build-ctor render mixins display-name))))
132 |
133 |
134 | (defn build-defcs [render-body mixins display-name]
135 | (let [render (fn [state] [(apply render-body state (:prum/args state)) state])]
136 | (build-ctor render mixins display-name)))
137 |
138 |
139 | (defn build-defcc [render-body mixins display-name]
140 | (let [render (fn [state] [(apply render-body (:prum/react-component state) (:prum/args state)) state])]
141 | (build-ctor render mixins display-name)))
142 |
143 |
144 | ;; prum.core APIs
145 |
146 |
147 | (defn with-key [element key]
148 | (cond
149 | (render/nothing? element)
150 | element
151 |
152 | (map? (get element 1))
153 | (assoc-in element [1 :key] key)
154 |
155 | :else
156 | (into [(first element) {:key key}] (next element))))
157 |
158 |
159 | (defn with-ref [element ref]
160 | element)
161 |
162 | (defn use-ref [component key]
163 | key)
164 |
165 |
166 | ;; mixins
167 |
168 |
169 | (def static {})
170 |
171 |
172 | (defn local
173 | ([initial] (local initial :prum/local))
174 | ([initial key]
175 | {:will-mount (fn [state]
176 | (assoc state key (atom initial)))}))
177 |
178 |
179 | (def reactive {})
180 |
181 |
182 | (def react deref)
183 |
184 |
185 | (defn cursor-in
186 | "Given atom with deep nested value and path inside it, creates an atom-like structure
187 | that can be used separately from main atom, but will sync changes both ways:
188 |
189 | (def db (atom { :users { \"Ivan\" { :age 30 }}}))
190 | (def ivan (prum/cursor db [:users \"Ivan\"]))
191 | \\@ivan ;; => { :age 30 }
192 | (swap! ivan update :age inc) ;; => { :age 31 }
193 | \\@db ;; => { :users { \"Ivan\" { :age 31 }}}
194 | (swap! db update-in [:users \"Ivan\" :age] inc) ;; => { :users { \"Ivan\" { :age 32 }}}
195 | \\@ivan ;; => { :age 32 }
196 |
197 | Returned value supports deref, swap!, reset!, watches and metadata.
198 | The only supported option is `:meta`"
199 | ^prum.cursor.Cursor [ref path & {:as options}]
200 | (if (instance? Cursor ref)
201 | (cursor/Cursor. (.-ref ^Cursor ref) (into (.-path ^Cursor ref) path) (:meta options) (volatile! {}))
202 | (cursor/Cursor. ref path (:meta options) (volatile! {}))))
203 |
204 |
205 | (defn cursor
206 | "Same as `prum.core/cursor-in` but accepts single key instead of path vector"
207 | ^prum.cursor.Cursor [ref key & options]
208 | (apply cursor-in ref [key] options))
209 |
210 | (def ^{:style/indent 2} derived-atom
211 | "Use this to create “chains” and acyclic graphs of dependent atoms.
212 | `derived-atom` will:
213 | - Take N “source” refs
214 | - Set up a watch on each of them
215 | - Create “sink” atom
216 | - When any of source refs changes:
217 | - re-run function `f`, passing N dereferenced values of source refs
218 | - `reset!` result of `f` to the sink atom
219 | - return sink atom
220 |
221 | (def *a (atom 0))
222 | (def *b (atom 1))
223 | (def *x (derived-atom [*a *b] ::key
224 | (fn [a b]
225 | (str a \":\" b))))
226 | (type *x) ;; => clojure.lang.Atom
227 | \\@*x ;; => 0:1
228 | (swap! *a inc)
229 | \\@*x ;; => 1:1
230 | (reset! *b 7)
231 | \\@*x ;; => 1:7
232 |
233 | Arguments:
234 | refs - sequence of source refs
235 | key - unique key to register watcher, see `clojure.core/add-watch`
236 | f - function that must accept N arguments (same as number of source refs)
237 | and return a value to be written to the sink ref.
238 | Note: `f` will be called with already dereferenced values
239 | opts - optional. Map of:
240 | :ref - Use this as sink ref. By default creates new atom
241 | :check-equals? - Do an equality check on each update: `(= @sink (f new-vals))`.
242 | If result of `f` is equal to the old one, do not call `reset!`.
243 | Defaults to `true`. Set to false if calling `=` would be expensive"
244 | derived-atom/derived-atom)
245 |
246 |
247 | ;;; Server-side rendering
248 |
249 |
250 | (def render-html
251 | "Main server-side rendering method. Given component, returns HTML string with
252 | static markup of that component. Serve that string to the browser and
253 | `prum.core/mount` same Rum component over it. React will be able to reuse already
254 | existing DOM and will initialize much faster"
255 | render/render-html)
256 |
257 | (def render-static-markup
258 | "Same as `prum.core/render-html` but returned string has nothing React-specific.
259 | This allows Rum to be used as traditional server-side template engine"
260 | render/render-static-markup)
261 |
262 |
263 | ;; method parity with CLJS version so you can avoid conditional directive
264 | ;; in e.g. did-mount/will-unmount mixin bodies
265 |
266 |
267 | (defn state [c]
268 | (throw (UnsupportedOperationException. "state is only available from ClojureScript")))
269 |
270 |
271 | (defn dom-node [s]
272 | (throw (UnsupportedOperationException. "dom-node is only available from ClojureScript")))
273 |
274 |
275 | (defn ref [s k]
276 | (throw (UnsupportedOperationException. "ref is only available from ClojureScript")))
277 |
278 |
279 | (defn ref-node [s k]
280 | (throw (UnsupportedOperationException. "ref is only available from ClojureScript")))
281 |
282 |
283 | (defn context [component key]
284 | (throw (UnsupportedOperationException. "context is only available from ClojureScript")))
285 |
286 |
287 | (defn mount [c n]
288 | (throw (UnsupportedOperationException. "mount is only available from ClojureScript")))
289 |
290 |
291 | (defn unmount [c]
292 | (throw (UnsupportedOperationException. "unmount is only available from ClojureScript")))
293 |
294 |
295 | (defn request-render [c]
296 | (throw (UnsupportedOperationException. "request-render is only available from ClojureScript")))
297 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Eclipse Public License - v 1.0
2 |
3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC
4 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM
5 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
6 |
7 | 1. DEFINITIONS
8 |
9 | "Contribution" means:
10 |
11 | a) in the case of the initial Contributor, the initial code and documentation
12 | distributed under this Agreement, and
13 | b) in the case of each subsequent Contributor:
14 | i) changes to the Program, and
15 | ii) additions to the Program;
16 |
17 | where such changes and/or additions to the Program originate from and are
18 | distributed by that particular Contributor. A Contribution 'originates'
19 | from a Contributor if it was added to the Program by such Contributor
20 | itself or anyone acting on such Contributor's behalf. Contributions do not
21 | include additions to the Program which: (i) are separate modules of
22 | software distributed in conjunction with the Program under their own
23 | license agreement, and (ii) are not derivative works of the Program.
24 |
25 | "Contributor" means any person or entity that distributes the Program.
26 |
27 | "Licensed Patents" mean patent claims licensable by a Contributor which are
28 | necessarily infringed by the use or sale of its Contribution alone or when
29 | combined with the Program.
30 |
31 | "Program" means the Contributions distributed in accordance with this
32 | Agreement.
33 |
34 | "Recipient" means anyone who receives the Program under this Agreement,
35 | including all Contributors.
36 |
37 | 2. GRANT OF RIGHTS
38 | a) Subject to the terms of this Agreement, each Contributor hereby grants
39 | Recipient a non-exclusive, worldwide, royalty-free copyright license to
40 | reproduce, prepare derivative works of, publicly display, publicly
41 | perform, distribute and sublicense the Contribution of such Contributor,
42 | if any, and such derivative works, in source code and object code form.
43 | b) Subject to the terms of this Agreement, each Contributor hereby grants
44 | Recipient a non-exclusive, worldwide, royalty-free patent license under
45 | Licensed Patents to make, use, sell, offer to sell, import and otherwise
46 | transfer the Contribution of such Contributor, if any, in source code and
47 | object code form. This patent license shall apply to the combination of
48 | the Contribution and the Program if, at the time the Contribution is
49 | added by the Contributor, such addition of the Contribution causes such
50 | combination to be covered by the Licensed Patents. The patent license
51 | shall not apply to any other combinations which include the Contribution.
52 | No hardware per se is licensed hereunder.
53 | c) Recipient understands that although each Contributor grants the licenses
54 | to its Contributions set forth herein, no assurances are provided by any
55 | Contributor that the Program does not infringe the patent or other
56 | intellectual property rights of any other entity. Each Contributor
57 | disclaims any liability to Recipient for claims brought by any other
58 | entity based on infringement of intellectual property rights or
59 | otherwise. As a condition to exercising the rights and licenses granted
60 | hereunder, each Recipient hereby assumes sole responsibility to secure
61 | any other intellectual property rights needed, if any. For example, if a
62 | third party patent license is required to allow Recipient to distribute
63 | the Program, it is Recipient's responsibility to acquire that license
64 | before distributing the Program.
65 | d) Each Contributor represents that to its knowledge it has sufficient
66 | copyright rights in its Contribution, if any, to grant the copyright
67 | license set forth in this Agreement.
68 |
69 | 3. REQUIREMENTS
70 |
71 | A Contributor may choose to distribute the Program in object code form under
72 | its own license agreement, provided that:
73 |
74 | a) it complies with the terms and conditions of this Agreement; and
75 | b) its license agreement:
76 | i) effectively disclaims on behalf of all Contributors all warranties
77 | and conditions, express and implied, including warranties or
78 | conditions of title and non-infringement, and implied warranties or
79 | conditions of merchantability and fitness for a particular purpose;
80 | ii) effectively excludes on behalf of all Contributors all liability for
81 | damages, including direct, indirect, special, incidental and
82 | consequential damages, such as lost profits;
83 | iii) states that any provisions which differ from this Agreement are
84 | offered by that Contributor alone and not by any other party; and
85 | iv) states that source code for the Program is available from such
86 | Contributor, and informs licensees how to obtain it in a reasonable
87 | manner on or through a medium customarily used for software exchange.
88 |
89 | When the Program is made available in source code form:
90 |
91 | a) it must be made available under this Agreement; and
92 | b) a copy of this Agreement must be included with each copy of the Program.
93 | Contributors may not remove or alter any copyright notices contained
94 | within the Program.
95 |
96 | Each Contributor must identify itself as the originator of its Contribution,
97 | if
98 | any, in a manner that reasonably allows subsequent Recipients to identify the
99 | originator of the Contribution.
100 |
101 | 4. COMMERCIAL DISTRIBUTION
102 |
103 | Commercial distributors of software may accept certain responsibilities with
104 | respect to end users, business partners and the like. While this license is
105 | intended to facilitate the commercial use of the Program, the Contributor who
106 | includes the Program in a commercial product offering should do so in a manner
107 | which does not create potential liability for other Contributors. Therefore,
108 | if a Contributor includes the Program in a commercial product offering, such
109 | Contributor ("Commercial Contributor") hereby agrees to defend and indemnify
110 | every other Contributor ("Indemnified Contributor") against any losses,
111 | damages and costs (collectively "Losses") arising from claims, lawsuits and
112 | other legal actions brought by a third party against the Indemnified
113 | Contributor to the extent caused by the acts or omissions of such Commercial
114 | Contributor in connection with its distribution of the Program in a commercial
115 | product offering. The obligations in this section do not apply to any claims
116 | or Losses relating to any actual or alleged intellectual property
117 | infringement. In order to qualify, an Indemnified Contributor must:
118 | a) promptly notify the Commercial Contributor in writing of such claim, and
119 | b) allow the Commercial Contributor to control, and cooperate with the
120 | Commercial Contributor in, the defense and any related settlement
121 | negotiations. The Indemnified Contributor may participate in any such claim at
122 | its own expense.
123 |
124 | For example, a Contributor might include the Program in a commercial product
125 | offering, Product X. That Contributor is then a Commercial Contributor. If
126 | that Commercial Contributor then makes performance claims, or offers
127 | warranties related to Product X, those performance claims and warranties are
128 | such Commercial Contributor's responsibility alone. Under this section, the
129 | Commercial Contributor would have to defend claims against the other
130 | Contributors related to those performance claims and warranties, and if a
131 | court requires any other Contributor to pay any damages as a result, the
132 | Commercial Contributor must pay those damages.
133 |
134 | 5. NO WARRANTY
135 |
136 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN
137 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR
138 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE,
139 | NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each
140 | Recipient is solely responsible for determining the appropriateness of using
141 | and distributing the Program and assumes all risks associated with its
142 | exercise of rights under this Agreement , including but not limited to the
143 | risks and costs of program errors, compliance with applicable laws, damage to
144 | or loss of data, programs or equipment, and unavailability or interruption of
145 | operations.
146 |
147 | 6. DISCLAIMER OF LIABILITY
148 |
149 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY
150 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL,
151 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION
152 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
153 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
154 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE
155 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY
156 | OF SUCH DAMAGES.
157 |
158 | 7. GENERAL
159 |
160 | If any provision of this Agreement is invalid or unenforceable under
161 | applicable law, it shall not affect the validity or enforceability of the
162 | remainder of the terms of this Agreement, and without further action by the
163 | parties hereto, such provision shall be reformed to the minimum extent
164 | necessary to make such provision valid and enforceable.
165 |
166 | If Recipient institutes patent litigation against any entity (including a
167 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself
168 | (excluding combinations of the Program with other software or hardware)
169 | infringes such Recipient's patent(s), then such Recipient's rights granted
170 | under Section 2(b) shall terminate as of the date such litigation is filed.
171 |
172 | All Recipient's rights under this Agreement shall terminate if it fails to
173 | comply with any of the material terms or conditions of this Agreement and does
174 | not cure such failure in a reasonable period of time after becoming aware of
175 | such noncompliance. If all Recipient's rights under this Agreement terminate,
176 | Recipient agrees to cease use and distribution of the Program as soon as
177 | reasonably practicable. However, Recipient's obligations under this Agreement
178 | and any licenses granted by Recipient relating to the Program shall continue
179 | and survive.
180 |
181 | Everyone is permitted to copy and distribute copies of this Agreement, but in
182 | order to avoid inconsistency the Agreement is copyrighted and may only be
183 | modified in the following manner. The Agreement Steward reserves the right to
184 | publish new versions (including revisions) of this Agreement from time to
185 | time. No one other than the Agreement Steward has the right to modify this
186 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The
187 | Eclipse Foundation may assign the responsibility to serve as the Agreement
188 | Steward to a suitable separate entity. Each new version of the Agreement will
189 | be given a distinguishing version number. The Program (including
190 | Contributions) may always be distributed subject to the version of the
191 | Agreement under which it was received. In addition, after a new version of the
192 | Agreement is published, Contributor may elect to distribute the Program
193 | (including its Contributions) under the new version. Except as expressly
194 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or
195 | licenses to the intellectual property of any Contributor under this Agreement,
196 | whether expressly, by implication, estoppel or otherwise. All rights in the
197 | Program not expressly granted under this Agreement are reserved.
198 |
199 | This Agreement is governed by the laws of the State of New York and the
200 | intellectual property laws of the United States of America. No party to this
201 | Agreement will bring a legal action under this Agreement more than one year
202 | after the cause of action arose. Each party waives its rights to a jury trial in
203 | any resulting litigation.
204 |
--------------------------------------------------------------------------------
/src/prum/core.cljs:
--------------------------------------------------------------------------------
1 | (ns prum.core
2 | (:refer-clojure :exclude [ref])
3 | (:require-macros prum.core)
4 | (:require
5 | ["prum-preact" :as p]
6 | [goog.object :as gobj]
7 | [goog.dom :as gdom]
8 | [goog.functions :as gf]
9 | [prum.cursor :as cursor]
10 | [prum.util :as util :refer [collect collect* call-all]]
11 | [prum.derived-atom :as derived-atom]))
12 |
13 | (defn state
14 | "Given React component, returns Rum state associated with it"
15 | [comp]
16 | (gobj/get (.-state comp) ":prum/state"))
17 |
18 | (defn extend! [obj props]
19 | (doseq [[k v] props
20 | :when (some? v)]
21 | (gobj/set obj (name k) (clj->js v))))
22 |
23 |
24 | (defn- build-class [render mixins display-name]
25 | (let [init (collect :init mixins) ;; state props -> state
26 | will-mount (collect* [:will-mount ;; state -> state
27 | :before-render] mixins) ;; state -> state
28 | render render ;; state -> [dom state]
29 | wrap-render (collect :wrap-render mixins) ;; render-fn -> render-fn
30 | wrapped-render (reduce #(%2 %1) render wrap-render)
31 | did-mount (collect* [:did-mount ;; state -> state
32 | :after-render] mixins) ;; state -> state
33 | did-remount (collect :did-remount mixins) ;; old-state state -> state
34 | should-update (collect :should-update mixins) ;; old-state state -> boolean
35 | will-update (collect* [:will-update ;; state -> state
36 | :before-render] mixins) ;; state -> state
37 | did-update (collect* [:did-update ;; state -> state
38 | :after-render] mixins) ;; state -> state
39 | will-unmount (collect :will-unmount mixins) ;; state -> state
40 | child-context (collect :child-context mixins) ;; state -> child-context
41 | class-props (reduce merge (collect :class-properties mixins)) ;; custom properties and methods
42 | static-props (reduce merge (collect :static-properties mixins)) ;; custom static properties and methods
43 |
44 | ctor (fn [props]
45 | (this-as this
46 | (set! (.. this -state)
47 | #js {":prum/state"
48 | (-> (gobj/get props ":prum/initial-state")
49 | (assoc :prum/react-component this)
50 | (call-all init props)
51 | volatile!)})
52 | (set! (.. this -refs) {})
53 | (.call p/Component this props)))
54 | _ (goog/inherits ctor p/Component)
55 | prototype (gobj/get ctor "prototype")]
56 |
57 | (when-not (empty? will-mount)
58 | (set! (.. prototype -componentWillMount)
59 | (fn []
60 | (this-as this
61 | (vswap! (state this) call-all will-mount)))))
62 |
63 | (when-not (empty? did-mount)
64 | (set! (.. prototype -componentDidMount)
65 | (fn []
66 | (this-as this
67 | (vswap! (state this) call-all did-mount)))))
68 |
69 | (set! (.. prototype -componentWillReceiveProps)
70 | (fn [next-props]
71 | (this-as this
72 | (let [old-state @(state this)
73 | state (merge old-state
74 | (gobj/get next-props ":prum/initial-state"))
75 | next-state (reduce #(%2 old-state %1) state did-remount)]
76 | ;; allocate new volatile so that we can access both old and new states in shouldComponentUpdate
77 | (.setState this #js {":prum/state" (volatile! next-state)})))))
78 |
79 | (when-not (empty? should-update)
80 | (set! (.. prototype -shouldComponentUpdate)
81 | (fn [next-props next-state]
82 | (this-as this
83 | (let [old-state @(state this)
84 | new-state @(gobj/get next-state ":prum/state")]
85 | (or (some #(% old-state new-state) should-update) false))))))
86 |
87 | (when-not (empty? will-update)
88 | (set! (.. prototype -componentWillUpdate)
89 | (fn [_ next-state]
90 | (this-as this
91 | (let [new-state (gobj/get next-state ":prum/state")]
92 | (vswap! new-state call-all will-update))))))
93 |
94 | (set! (.. prototype -render)
95 | (fn []
96 | (this-as this
97 | (let [state (state this)
98 | [dom next-state] (wrapped-render @state)]
99 | (vreset! state next-state)
100 | dom))))
101 |
102 | (when-not (empty? did-update)
103 | (set! (.. prototype -componentDidUpdate)
104 | (fn [_ _]
105 | (this-as this
106 | (vswap! (state this) call-all did-update)))))
107 |
108 | (set! (.. prototype -componentWillUnmount)
109 | (fn []
110 | (this-as this
111 | (when-not (empty? will-unmount)
112 | (vswap! (state this) call-all will-unmount))
113 | (gobj/set this ":prum/unmounted?" true))))
114 |
115 | (when-not (empty? child-context)
116 | (set! (.. prototype -getChildContext)
117 | (fn []
118 | (this-as this
119 | (let [state @(state this)]
120 | (clj->js (transduce (map #(% state)) merge {} child-context)))))))
121 |
122 | (extend! prototype class-props)
123 | (set! (.. ctor -displayName) display-name)
124 | (extend! ctor static-props)
125 | ctor))
126 |
127 |
128 | (defn- build-ctor [render mixins display-name]
129 | (let [class (build-class render mixins display-name)
130 | key-fn (first (collect :key-fn mixins))
131 | ctor (if (some? key-fn)
132 | (fn [& args]
133 | (let [props #js {":prum/initial-state" {:prum/args args}
134 | :key (apply key-fn args)}]
135 | (p/createElement class props)))
136 | (fn [& args]
137 | (let [props #js {":prum/initial-state" {:prum/args args}}]
138 | (p/createElement class props))))]
139 | (with-meta ctor {:prum/class class})))
140 |
141 |
142 | (defn build-defc [render-body mixins display-name]
143 | (if (empty? mixins)
144 | (let [class (fn [props]
145 | (apply render-body (gobj/get props ":prum/args")))
146 | _ (set! (.. class -displayName) display-name)
147 | ctor (fn [& args]
148 | (p/createElement class #js {":prum/args" args}))]
149 | (with-meta ctor {:prum/class class}))
150 | (let [render (fn [state] [(apply render-body (:prum/args state)) state])]
151 | (build-ctor render mixins display-name))))
152 |
153 |
154 | (defn build-defcs [render-body mixins display-name]
155 | (let [render (fn [state] [(apply render-body state (:prum/args state)) state])]
156 | (build-ctor render mixins display-name)))
157 |
158 |
159 | (defn build-defcc [render-body mixins display-name]
160 | (let [render (fn [state] [(apply render-body (:prum/react-component state) (:prum/args state)) state])]
161 | (build-ctor render mixins display-name)))
162 |
163 | (defn- set-meta [ctor]
164 | (let [f #(let [ctor (ctor)]
165 | (.apply ctor ctor (js-arguments)))]
166 | (specify! f IMeta (-meta [_] (meta ctor)))
167 | f))
168 |
169 | (defn lazy-component [builder render mixins display-name]
170 | (let [bf #(builder render mixins display-name)
171 | ctor (gf/cacheReturnValue bf)]
172 | (set-meta ctor)))
173 |
174 |
175 | (defn request-render
176 | "Re-render preact component"
177 | [component]
178 | (when-not (gobj/get component ":prum/unmounted?")
179 | (.forceUpdate component)))
180 |
181 |
182 | (defn mount
183 | "Add component to the DOM tree. Idempotent. Subsequent mounts will just update component"
184 | ([component node]
185 | (mount component node nil))
186 | ([component node root]
187 | (let [root (p/render component node root)]
188 | (gobj/set node ":prum/root-node" root)
189 | root)))
190 |
191 | (defn unmount
192 | "Removes component from the DOM tree"
193 | [node]
194 | (let [root (gobj/get node ":prum/root-node")
195 | parent (when root (gdom/getParentElement root))]
196 | (if (= parent node)
197 | (do
198 | (p/render (p/createElement (constantly nil)) parent root)
199 | true)
200 | false)))
201 |
202 | ;; initialization
203 |
204 | (defn with-key
205 | "Adds React key to component"
206 | [component key]
207 | (p/cloneElement component #js {:key key}))
208 |
209 | (defn with-ref
210 | "Adds React ref to component"
211 | [component ref]
212 | (p/cloneElement component #js {:ref ref}))
213 |
214 | (defn use-ref
215 | "Adds node to component as React ref"
216 | [component key]
217 | (fn [node]
218 | (let [refs (.-refs component)]
219 | (->> (assoc refs key node)
220 | (set! (.. component -refs))))))
221 |
222 | (defn ref
223 | "Given state and ref handle, returns React component"
224 | [state key]
225 | (-> state :prum/react-component .-refs (get key)))
226 |
227 | (defn ref-node
228 | "Given state and ref handle, returns DOM node associated with ref"
229 | [state key]
230 | (when-let [ref (ref state key)]
231 | (.-base ref)))
232 |
233 | (defn dom-node
234 | "Given state, returns top-level DOM node. Can’t be called during render"
235 | [state]
236 | (.-base (:prum/react-component state)))
237 |
238 | (defn context [component key]
239 | (-> component
240 | .-context
241 | (gobj/get (name key))))
242 |
243 |
244 | ;; static mixin
245 |
246 | (def static
247 | "Mixin. Will avoid re-render if none of component’s arguments have changed.
248 | Does equality check (=) on all arguments"
249 | {:should-update
250 | (fn [old-state new-state]
251 | (not= (:prum/args old-state) (:prum/args new-state)))})
252 |
253 |
254 | ;; local mixin
255 |
256 | (defn local
257 | "Mixin constructor. Adds an atom to component’s state that can be used to keep stuff
258 | during component’s lifecycle. Component will be re-rendered if atom’s value changes.
259 | Atom is stored under user-provided key or under `:prum/local` by default"
260 | ([initial] (local initial :prum/local))
261 | ([initial key]
262 | {:will-mount
263 | (fn [state]
264 | (let [local-state (atom initial)
265 | component (:prum/react-component state)]
266 | (add-watch local-state key
267 | (fn [_ _ _ _]
268 | (request-render component)))
269 | (assoc state key local-state)))}))
270 |
271 |
272 | ;; reactive mixin
273 |
274 | (def ^:private ^:dynamic *reactions*)
275 |
276 |
277 | (def reactive
278 | "Mixin. Works in conjunction with `prum.core/react`"
279 | {:init
280 | (fn [state props]
281 | (assoc state :prum.reactive/key (random-uuid)))
282 | :wrap-render
283 | (fn [render-fn]
284 | (fn [state]
285 | (binding [*reactions* (volatile! #{})]
286 | (let [comp (:prum/react-component state)
287 | old-reactions (:prum.reactive/refs state #{})
288 | [dom next-state] (render-fn state)
289 | new-reactions @*reactions*
290 | key (:prum.reactive/key state)]
291 | (doseq [ref old-reactions]
292 | (when-not (contains? new-reactions ref)
293 | (remove-watch ref key)))
294 | (doseq [ref new-reactions]
295 | (when-not (contains? old-reactions ref)
296 | (add-watch ref key
297 | (fn [_ _ _ _]
298 | (request-render comp)))))
299 | [dom (assoc next-state :prum.reactive/refs new-reactions)]))))
300 | :will-unmount
301 | (fn [state]
302 | (let [key (:prum.reactive/key state)]
303 | (doseq [ref (:prum.reactive/refs state)]
304 | (remove-watch ref key)))
305 | (dissoc state :prum.reactive/refs :prum.reactive/key))})
306 |
307 |
308 | (defn react
309 | "Works in conjunction with `prum.core/reactive` mixin. Use this function instead of
310 | `deref` inside render, and your component will subscribe to changes happening
311 | to the derefed atom."
312 | [ref]
313 | (assert *reactions* "prum.core/react is only supported in conjunction with prum.core/reactive")
314 | (vswap! *reactions* conj ref)
315 | @ref)
316 |
317 |
318 | ;; derived-atom
319 |
320 | (def ^{:style/indent 2} derived-atom
321 | "Use this to create “chains” and acyclic graphs of dependent atoms.
322 | `derived-atom` will:
323 | - Take N “source” refs
324 | - Set up a watch on each of them
325 | - Create “sink” atom
326 | - When any of source refs changes:
327 | - re-run function `f`, passing N dereferenced values of source refs
328 | - `reset!` result of `f` to the sink atom
329 | - return sink atom
330 |
331 | (def *a (atom 0))
332 | (def *b (atom 1))
333 | (def *x (derived-atom [*a *b] ::key
334 | (fn [a b]
335 | (str a \":\" b))))
336 | (type *x) ;; => clojure.lang.Atom
337 | \\@*x ;; => 0:1
338 | (swap! *a inc)
339 | \\@*x ;; => 1:1
340 | (reset! *b 7)
341 | \\@*x ;; => 1:7
342 |
343 | Arguments:
344 | refs - sequence of source refs
345 | key - unique key to register watcher, see `clojure.core/add-watch`
346 | f - function that must accept N arguments (same as number of source refs)
347 | and return a value to be written to the sink ref.
348 | Note: `f` will be called with already dereferenced values
349 | opts - optional. Map of:
350 | :ref - Use this as sink ref. By default creates new atom
351 | :check-equals? - Do an equality check on each update: `(= @sink (f new-vals))`.
352 | If result of `f` is equal to the old one, do not call `reset!`.
353 | Defaults to `true`. Set to false if calling `=` would be expensive"
354 | derived-atom/derived-atom)
355 |
356 |
357 | ;; cursors
358 |
359 | (defn cursor-in
360 | "Given atom with deep nested value and path inside it, creates an atom-like structure
361 | that can be used separately from main atom, but will sync changes both ways:
362 |
363 | (def db (atom { :users { \"Ivan\" { :age 30 }}}))
364 | (def ivan (prum/cursor db [:users \"Ivan\"]))
365 | \\@ivan ;; => { :age 30 }
366 | (swap! ivan update :age inc) ;; => { :age 31 }
367 | \\@db ;; => { :users { \"Ivan\" { :age 31 }}}
368 | (swap! db update-in [:users \"Ivan\" :age] inc) ;; => { :users { \"Ivan\" { :age 32 }}}
369 | \\@ivan ;; => { :age 32 }
370 |
371 | Returned value supports deref, swap!, reset!, watches and metadata.
372 | The only supported option is `:meta`"
373 | [ref path & {:as options}]
374 | (if (instance? cursor/Cursor ref)
375 | (cursor/Cursor. (gobj/get ref "ref") (into (gobj/get ref "path") path) (:meta options))
376 | (cursor/Cursor. ref path (:meta options))))
377 |
378 |
379 | (defn cursor
380 | "Same as `prum.core/cursor-in` but accepts single key instead of path vector"
381 | [ref key & options]
382 | (apply cursor-in ref [key] options))
383 |
--------------------------------------------------------------------------------
/src/prum/server_render.clj:
--------------------------------------------------------------------------------
1 | (ns prum.server-render
2 | (:require
3 | [clojure.string :as str])
4 | (:import
5 | [clojure.lang IPersistentVector ISeq Named Numbers Ratio Keyword]))
6 |
7 |
8 | (defn nothing? [element]
9 | (and (vector? element)
10 | (= :prum/nothing (first element))))
11 |
12 |
13 | (def ^:dynamic *select-value*)
14 |
15 | (defn append!
16 | ([^StringBuilder sb s0] (.append sb s0))
17 | ([^StringBuilder sb s0 s1]
18 | (.append sb s0)
19 | (.append sb s1))
20 | ([^StringBuilder sb s0 s1 s2]
21 | (.append sb s0)
22 | (.append sb s1)
23 | (.append sb s2))
24 | ([^StringBuilder sb s0 s1 s2 s3]
25 | (.append sb s0)
26 | (.append sb s1)
27 | (.append sb s2)
28 | (.append sb s3))
29 | ([^StringBuilder sb s0 s1 s2 s3 s4]
30 | (.append sb s0)
31 | (.append sb s1)
32 | (.append sb s2)
33 | (.append sb s3)
34 | (.append sb s4)))
35 |
36 |
37 | (defprotocol ToString
38 | (^String to-str [x] "Convert a value into a string."))
39 |
40 |
41 | (extend-protocol ToString
42 | Keyword (to-str [k] (name k))
43 | Ratio (to-str [r] (str (float r)))
44 | String (to-str [s] s)
45 | Object (to-str [x] (str x))
46 | nil (to-str [_] ""))
47 |
48 |
49 | (def ^{:doc "A list of elements that must be rendered without a closing tag."
50 | :private true}
51 | void-tags
52 | #{"area" "base" "br" "col" "command" "embed" "hr" "img" "input" "keygen" "link"
53 | "meta" "param" "source" "track" "wbr"})
54 |
55 |
56 |
57 | (def normalized-attrs
58 | {;; special cases
59 | :default-checked "checked"
60 | :default-value "value"
61 |
62 | ;; https://github.com/facebook/react/blob/master/src/renderers/dom/shared/HTMLDOMPropertyConfig.js
63 | :accept-charset "accept-charset"
64 | :access-key "accesskey"
65 | :allow-full-screen "allowfullscreen"
66 | :allow-transparency "allowtransparency"
67 | :auto-complete "autocomplete"
68 | :auto-play "autoplay"
69 | :cell-padding "cellpadding"
70 | :cell-spacing "cellspacing"
71 | :char-set "charset"
72 | :class-id "classid"
73 | :col-span "colspan"
74 | :content-editable "contenteditable"
75 | :context-menu "contextmenu"
76 | :cross-origin "crossorigin"
77 | :date-time "datetime"
78 | :enc-type "enctype"
79 | :form-action "formaction"
80 | :form-enc-type "formenctype"
81 | :form-method "formmethod"
82 | :form-no-validate "formnovalidate"
83 | :form-target "formtarget"
84 | :frame-border "frameborder"
85 | :href-lang "hreflang"
86 | :http-equiv "http-equiv"
87 | :input-mode "inputmode"
88 | :key-params "keyparams"
89 | :key-type "keytype"
90 | :margin-height "marginheight"
91 | :margin-width "marginwidth"
92 | :max-length "maxlength"
93 | :media-group "mediagroup"
94 | :min-length "minlength"
95 | :no-validate "novalidate"
96 | :radio-group "radiogroup"
97 | :referrer-policy "referrerpolicy"
98 | :read-only "readonly"
99 | :row-span "rowspan"
100 | :spell-check "spellcheck"
101 | :src-doc "srcdoc"
102 | :src-lang "srclang"
103 | :src-set "srcset"
104 | :tab-index "tabindex"
105 | :use-map "usemap"
106 | :auto-capitalize "autocapitalize"
107 | :auto-correct "autocorrect"
108 | :auto-save "autosave"
109 | :item-prop "itemprop"
110 | :item-scope "itemscope"
111 | :item-type "itemtype"
112 | :item-id "itemid"
113 | :item-ref "itemref"
114 |
115 | ;; https://github.com/facebook/react/blob/master/src/renderers/dom/shared/SVGDOMPropertyConfig.js
116 | :allow-reorder "allowReorder"
117 | :attribute-name "attributeName"
118 | :attribute-type "attributeType"
119 | :auto-reverse "autoReverse"
120 | :base-frequency "baseFrequency"
121 | :base-profile "baseProfile"
122 | :calc-mode "calcMode"
123 | :clip-path-units "clipPathUnits"
124 | :content-script-type "contentScriptType"
125 | :content-style-type "contentStyleType"
126 | :diffuse-constant "diffuseConstant"
127 | :edge-mode "edgeMode"
128 | :external-resources-required "externalResourcesRequired"
129 | :filter-res "filterRes"
130 | :filter-units "filterUnits"
131 | :glyph-ref "glyphRef"
132 | :gradient-transform "gradientTransform"
133 | :gradient-units "gradientUnits"
134 | :kernel-matrix "kernelMatrix"
135 | :kernel-unit-length "kernelUnitLength"
136 | :key-points "keyPoints"
137 | :key-splines "keySplines"
138 | :key-times "keyTimes"
139 | :length-adjust "lengthAdjust"
140 | :limiting-cone-angle "limitingConeAngle"
141 | :marker-height "markerHeight"
142 | :marker-units "markerUnits"
143 | :marker-width "markerWidth"
144 | :mask-content-units "maskContentUnits"
145 | :mask-units "maskUnits"
146 | :num-octaves "numOctaves"
147 | :path-length "pathLength"
148 | :pattern-content-units "patternContentUnits"
149 | :pattern-transform "patternTransform"
150 | :pattern-units "patternUnits"
151 | :points-at-x "pointsAtX"
152 | :points-at-y "pointsAtY"
153 | :points-at-z "pointsAtZ"
154 | :preserve-alpha "preserveAlpha"
155 | :preserve-aspect-ratio "preserveAspectRatio"
156 | :primitive-units "primitiveUnits"
157 | :ref-x "refX"
158 | :ref-y "refY"
159 | :repeat-count "repeatCount"
160 | :repeat-dur "repeatDur"
161 | :required-extensions "requiredExtensions"
162 | :required-features "requiredFeatures"
163 | :specular-constant "specularConstant"
164 | :specular-exponent "specularExponent"
165 | :spread-method "spreadMethod"
166 | :start-offset "startOffset"
167 | :std-deviation "stdDeviation"
168 | :stitch-tiles "stitchTiles"
169 | :surface-scale "surfaceScale"
170 | :system-language "systemLanguage"
171 | :table-values "tableValues"
172 | :target-x "targetX"
173 | :target-y "targetY"
174 | :view-box "viewBox"
175 | :view-target "viewTarget"
176 | :x-channel-selector "xChannelSelector"
177 | :xlink-actuate "xlink:actuate"
178 | :xlink-arcrole "xlink:arcrole"
179 | :xlink-href "xlink:href"
180 | :xlink-role "xlink:role"
181 | :xlink-show "xlink:show"
182 | :xlink-title "xlink:title"
183 | :xlink-type "xlink:type"
184 | :xml-base "xml:base"
185 | :xmlns-xlink "xmlns:xlink"
186 | :xml-lang "xml:lang"
187 | :xml-space "xml:space"
188 | :y-channel-selector "yChannelSelector"
189 | :zoom-and-pan "zoomAndPan"})
190 |
191 |
192 | (defn get-value [attrs]
193 | (or (:value attrs)
194 | (:default-value attrs)))
195 |
196 |
197 | (defn normalize-attr-key ^String [key]
198 | (or (normalized-attrs key)
199 | (name key)))
200 |
201 |
202 | (defn escape-html [^String s]
203 | (let [len (count s)]
204 | (loop [^StringBuilder sb nil
205 | i (int 0)]
206 | (if (< i len)
207 | (let [char (.charAt s i)
208 | repl (case char
209 | \& "&"
210 | \< "<"
211 | \> ">"
212 | \" """
213 | \' "'"
214 | nil)]
215 | (if (nil? repl)
216 | (if (nil? sb)
217 | (recur nil (inc i))
218 | (recur (doto sb
219 | (.append char))
220 | (inc i)))
221 | (if (nil? sb)
222 | (recur (doto (StringBuilder.)
223 | (.append s 0 i)
224 | (.append repl))
225 | (inc i))
226 | (recur (doto sb
227 | (.append repl))
228 | (inc i)))))
229 | (if (nil? sb) s (str sb))))))
230 |
231 |
232 | (defn parse-selector [s]
233 | (loop [matches (re-seq #"([#.])?([^#.]+)" (name s))
234 | tag "div"
235 | id nil
236 | classes nil]
237 | (if-let [[_ prefix val] (first matches)]
238 | (case prefix
239 | nil (recur (next matches) val id classes)
240 | "#" (recur (next matches) tag val classes)
241 | "." (recur (next matches) tag id (conj (or classes []) val)))
242 | [tag id classes])))
243 |
244 |
245 | (defn normalize-element [[first second & rest]]
246 | (when-not (or (keyword? first)
247 | (symbol? first)
248 | (string? first))
249 | (throw (ex-info "Expected a keyword as a tag" {:tag first})))
250 | (let [[tag tag-id tag-classes] (parse-selector first)
251 | [attrs children] (if (or (map? second)
252 | (nil? second))
253 | [second rest]
254 | [nil (cons second rest)])
255 | attrs-classes (:class attrs)
256 | classes (if (and tag-classes attrs-classes)
257 | [tag-classes attrs-classes]
258 | (or tag-classes attrs-classes))]
259 | [tag tag-id classes attrs children]))
260 |
261 |
262 | ;;; render attributes
263 |
264 |
265 | ;; https://github.com/facebook/react/blob/master/src/renderers/dom/shared/CSSProperty.js
266 | (def unitless-css-props
267 | (into #{}
268 | (for [key ["animation-iteration-count" "box-flex" "box-flex-group" "box-ordinal-group" "column-count" "flex" "flex-grow" "flex-positive" "flex-shrink" "flex-negative" "flex-order" "grid-row" "grid-column" "font-weight" "line-clamp" "line-height" "opacity" "order" "orphans" "tab-size" "widows" "z-index" "zoom" "fill-opacity" "stop-opacity" "stroke-dashoffset" "stroke-opacity" "stroke-width"]
269 | prefix ["" "-webkit-" "-ms-" "-moz-" "-o-"]]
270 | (str prefix key))))
271 |
272 |
273 | (defn normalize-css-key [k]
274 | (-> (to-str k)
275 | (str/replace #"[A-Z]" (fn [ch] (str "-" (str/lower-case ch))))
276 | (str/replace #"^ms-" "-ms-")))
277 |
278 |
279 | (defn normalize-css-value [key value]
280 | (cond
281 | (contains? unitless-css-props key)
282 | (escape-html (to-str value))
283 | (number? value)
284 | (str value (when (not= 0 value) "px"))
285 | (and (string? value)
286 | (re-matches #"\s*\d+\s*" value))
287 | (recur key (-> value str/trim Long/parseLong))
288 | (and (string? value)
289 | (re-matches #"\s*\d+\.\d+\s*" value))
290 | (recur key (-> value str/trim Double/parseDouble))
291 | :else
292 | (escape-html (to-str value))))
293 |
294 |
295 | (defn render-style-kv! [sb empty? k v]
296 | (if v
297 | (do
298 | (when empty?
299 | (append! sb " style=\""))
300 | (let [key (normalize-css-key k)
301 | val (normalize-css-value key v)]
302 | (append! sb key ":" val ";"))
303 | false)
304 | empty?))
305 |
306 |
307 | (defn render-style! [map sb]
308 | (let [empty? (reduce-kv (partial render-style-kv! sb) true map)]
309 | (when-not empty?
310 | (append! sb "\""))))
311 |
312 |
313 | (defn render-class! [sb first? class]
314 | (cond
315 | (nil? class)
316 | first?
317 | (string? class)
318 | (do
319 | (when-not first?
320 | (append! sb " "))
321 | (append! sb class)
322 | false)
323 | (or (sequential? class)
324 | (set? class))
325 | (reduce #(render-class! sb %1 %2) first? class)
326 | :else
327 | (render-class! sb first? (to-str class))))
328 |
329 |
330 | (defn render-classes! [classes sb]
331 | (when classes
332 | (append! sb " class=\"")
333 | (render-class! sb true classes)
334 | (append! sb "\"")))
335 |
336 |
337 | (defn- render-attr-str! [sb attr value]
338 | (append! sb " " attr "=\"" (escape-html (to-str value)) "\""))
339 |
340 |
341 | (defn render-attr! [tag key value sb]
342 | (let [attr (normalize-attr-key key)]
343 | (cond
344 | (= "type" attr) :nop ;; rendered manually in render-element! before id
345 | (= "style" attr) (render-style! value sb)
346 | (= "key" attr) :nop
347 | (= "ref" attr) :nop
348 | (= "class" attr) :nop
349 | (and (= "value" attr)
350 | (or (= "select" tag)
351 | (= "textarea" tag))) :nop
352 | (.startsWith attr "aria-") (render-attr-str! sb attr value)
353 | (not value) :nop
354 | (true? value) (append! sb " " attr "=\"\"")
355 | (.startsWith attr "on") :nop
356 | (= "dangerouslySetInnerHTML" attr) :nop
357 | :else (render-attr-str! sb attr value))))
358 |
359 |
360 | (defn render-attrs! [tag attrs sb]
361 | (reduce-kv (fn [_ k v] (render-attr! tag k v sb)) nil attrs))
362 |
363 |
364 | ;;; render html
365 |
366 |
367 | (defprotocol HtmlRenderer
368 | (-render-html [this parent sb]
369 | "Turn a Clojure data type into a string of HTML with react ids."))
370 |
371 |
372 | (defn render-inner-html! [attrs children sb]
373 | (when-let [inner-html (:dangerouslySetInnerHTML attrs)]
374 | (when-not (empty? children)
375 | (throw (Exception. "Invariant Violation: Can only set one of `children` or `props.dangerouslySetInnerHTML`.")))
376 | (when-not (:__html inner-html)
377 | (throw (Exception. "Invariant Violation: `props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. Please visit https://fb.me/react-invariant-dangerously-set-inner-html for more information.")))
378 | (append! sb (:__html inner-html))
379 | true))
380 |
381 |
382 | (defn render-textarea-value! [tag attrs sb]
383 | (when (= tag "textarea")
384 | (when-some [value (get-value attrs)]
385 | (append! sb (escape-html value))
386 | true)))
387 |
388 |
389 | (defn render-content! [tag attrs children sb]
390 | (if (and (nil? children)
391 | (contains? void-tags tag))
392 | (append! sb "/>")
393 | (do
394 | (append! sb ">")
395 | (or (render-textarea-value! tag attrs sb)
396 | (render-inner-html! attrs children sb)
397 | (doseq [element children]
398 | (-render-html element children sb)))
399 | (append! sb "" tag ">"))))
400 |
401 |
402 | (defn render-element!
403 | "Render an element vector as a HTML element."
404 | [element sb]
405 | (when-not (nothing? element)
406 | (let [[tag id classes attrs children] (normalize-element element)]
407 | (append! sb "<" tag)
408 |
409 | (when-some [type (:type attrs)]
410 | (append! sb " type=\"" type "\""))
411 |
412 | (when (and (= "option" tag)
413 | (= (get-value attrs) *select-value*))
414 | (append! sb " selected=\"\""))
415 |
416 | (when id
417 | (append! sb " id=\"" id "\""))
418 |
419 | (render-attrs! tag attrs sb)
420 |
421 | (render-classes! classes sb)
422 |
423 | (if (= "select" tag)
424 | (binding [*select-value* (get-value attrs)]
425 | (render-content! tag attrs children sb))
426 | (render-content! tag attrs children sb)))))
427 |
428 |
429 | (extend-protocol HtmlRenderer
430 | IPersistentVector
431 | (-render-html [this parent sb]
432 | (render-element! this sb))
433 |
434 | ISeq
435 | (-render-html [this parent sb]
436 | (doseq [element this]
437 | (-render-html element parent sb)))
438 |
439 | Named
440 | (-render-html [this parent sb]
441 | (append! sb (name this)))
442 |
443 | String
444 | (-render-html [this parent sb]
445 | (append! sb (escape-html this)))
446 |
447 | Object
448 | (-render-html [this parent sb]
449 | (-render-html (str this) parent sb))
450 |
451 | nil
452 | (-render-html [this parent sb]
453 | :nop))
454 |
455 |
456 | (defn render-static-markup [src]
457 | (let [sb (StringBuilder.)]
458 | (-render-html src nil sb)
459 | (str sb)))
460 |
461 | (def render-html render-static-markup)
462 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | _Prum (Pale Rum) is a fork of Rum library that uses Preact.js as an underlying UI rendering facility_
2 |
3 | `[org.roman01la/prum "0.10.8-11"]`
4 |
5 | ### Differences to Rum/React
6 |
7 | #### No Hiccup interpretation
8 |
9 | Similar to Rum, Prum is using Hiccup compiler to transform Hiccup into JavaScript calls to Preact's API. However Hiccup interpretation is disabled in Prum, since it adds performance overhead.
10 |
11 | Due to this restrictions are the following:
12 |
13 | - Do not generate Hiccup elements programmatically
14 | - Wrap Hiccup elements with `prum.compiler/html` macro when returning Hiccup from a function
15 | - A list of forms that may contain Hiccup and will be handled by the compiler: [see here](https://github.com/rauhs/hicada/blob/master/src/hicada/compiler.clj#L111-L184)
16 | - If the first argument after the tag is a variable, it’s assumed to be the first child
17 |
18 | #### Creating ReactNodes from React class components
19 |
20 | ```clojure
21 | [:> ReactSelect {:value "value" :options options}]
22 |
23 | ;; (preact/createElement ReactSelect #js {:value value :options options})
24 | ```
25 |
26 | #### No string refs
27 |
28 | Preact supports only function _refs_. However string refs is still useful and easier to use in ClojureScript. To handle this properly there's a new helper function `rum.core/use-ref`
29 |
30 | ```clojure
31 | (rum/defc input []
32 | [:input {}])
33 |
34 | (rum/defcc form <
35 | {:after-render
36 | (fn [state]
37 | (rum/ref state :btn) ;; returns DOM node of the element
38 | (rum/ref state :input) ;; returns component
39 | (rum/ref-node state :input) ;; returns top-level DOM node of the component
40 | state)}
41 |
42 | [comp]
43 |
44 | [:form {}
45 | (rum/with-ref (input) (rum/use-ref comp :input))
46 | [:button {:ref (rum/use-ref comp :btn)} "text"]])
47 | ```
48 |
49 | #### Context API
50 |
51 | Preact components doesn't implement `contextTypes` and `childContextTypes` as in React. This means that in Prum there's no need to declare `:contextTypes` and `:childContextTypes` in `:class-properties` mixin.
52 |
53 | Also there's a helper function to read from context `rum.core/context`.
54 |
55 | ```clojure
56 | (rum/defcc rum-context-comp [comp]
57 | [:span
58 | {:style {:color (rum/context comp :color)}}
59 | "Child component uses context to set font color."])
60 |
61 | (rum/defc context <
62 | {:child-context (fn [state] {:color @core/*color})}
63 | []
64 | [:div {}
65 | [:div {} "Root component implicitly passes data to descendants."]
66 | (rum-context-comp)])
67 | ```
68 |
69 | #### Re-rendering
70 |
71 | When re-rendering from the root, by default Preact appends to root DOM node. To re-render properly `rum.core/mount` accepts optional third argument, which is the root node to replace.
72 |
73 | ```clojure
74 | (def root (rum/mount (app) dom-node)) ;; returns root DOM node
75 |
76 | (rum/mount (app) dom-node root) ;; pass in the root node to render and replace
77 | ```
78 |
79 | #### Collections of child elements
80 |
81 | When rendering a list of values, a collection of elements _should not be a vector_.
82 |
83 | ```clojure
84 | [:ul {} (mapv render-item items)] ;; this is wrong
85 | [:ul {} (map render-item items)] ;; this is ok
86 |
87 | [:ul {} [[:li {} "#1"] [:li {} "#2"]]] ;; this is wrong
88 | [:ul {} '([:li {} "#1"] [:li {} "#2"])] ;; this is ok
89 | ```
90 |
91 | #### Dynamic CSS via `:css` attribute
92 |
93 | Provided by [Clojure Style Sheets](https://github.com/roman01la/cljss) library.
94 |
95 | ```clojure
96 | [:button {:css {:font-size "12px"}}]
97 | ```
98 |
99 | #### DOM Events
100 |
101 | Preact use native (in-browser) event system instead of Synthetic Events system as in React, thus it doesn't change behaviour of DOM events. However to stay compatible with Rum/React, Prum translates `:on-change` handlers into `:on-input` as React does.
102 |
103 | _Below is original unmodified documentation of Rum_
104 |
105 |
106 |
107 | Rum is a client/server library for HTML UI. In ClojureScript, it works as React wrapper, in Clojure, it is a static HTML generator.
108 |
109 | ### Principles
110 |
111 | **Simple semantics**: Rum is arguably smaller, simpler and more straightforward than React itself.
112 |
113 | **Decomplected**: Rum is a library, not a framework. Use only the parts you need, throw away or replace what you don’t need, combine different approaches in a single app, or even combine Rum with other frameworks.
114 |
115 | **No enforced state model**: Unlike Om, Reagent or Quiescent, Rum does not dictate where to keep your state. Instead, it works well with any storage: persistent data structures, atoms, DataScript, JavaScript objects, localStorage or any custom solution you can think of.
116 |
117 | **Extensible**: the API is stable and explicitly defined, including the API between Rum internals. It lets you build custom behaviours that change components in significant ways.
118 |
119 | **Minimal codebase**: You can become a Rum expert just by reading its source code (~900 lines).
120 |
121 | ### Comparison to other frameworks
122 |
123 | Rum:
124 |
125 | - does not dictate how to store your state,
126 | - has server-side rendering,
127 | - is much smaller.
128 |
129 | ### Who’s using Rum?
130 |
131 | - [Cognician](https://www.cognician.com), coaching platform
132 | - [Attendify](https://attendify.com), mobile app builder
133 | - [PartsBox.io](https://partsbox.io), inventory management
134 | - [modnaKasta](https://modnaKasta.ua), online shopping
135 | - [ChildrensHeartSurgery.info](http://childrensheartsurgery.info), heart surgery statistics
136 | - [Mighty Hype](http://mightyhype.com/), cinema platform (server-side rendering)
137 | - [БезопасныеДороги.рф](https://xn--80abhddbmm5bieahtk5n.xn--p1ai/), road data aggregator
138 | - [TourneyBot](http://houstonindoor.com/2016), frisbee tournament app
139 | - [PurposeFly](https://www.purposefly.com/), HR 2.0 platform
140 |
141 | ## Using Rum
142 |
143 | Add to project.clj: `[rum "0.10.8"]`
144 |
145 | ### Defining a component
146 |
147 | Use `rum.core/defc` (short for “define component”) to define a function that returns component markup:
148 |
149 | ```clojure
150 | (require [rum.core :as rum])
151 |
152 | (rum/defc label [text]
153 | [:div {:class "label"} text])
154 | ```
155 |
156 | Rum uses Hiccup-like syntax for defining markup:
157 |
158 | ```clojure
159 | [? *]
160 | ```
161 |
162 | `` defines a tag, its id and classes:
163 |
164 | ```clojure
165 | :span
166 | :span#id
167 | :span.class
168 | :span#id.class
169 | :span.class.class2
170 | ```
171 |
172 | By default, if you omit the tag, `div` is assumed:
173 |
174 | ```
175 | :#id === :div#id
176 | :.class === :div.class
177 | ```
178 |
179 | `` is an optional map of attributes:
180 |
181 | - Use kebab-case keywords for attributes (e.g. `:allow-full-screen` for `allowFullScreen`)
182 | - You can include `:id` and `:class` there as well
183 | - `:class` can be a string or a sequence of strings
184 | - `:style`, if needed, must be a map with kebab-case keywords
185 | - event handlers should be arity-one functions
186 |
187 | ```clojure
188 | [:input { :type "text"
189 | :allow-full-screen true
190 | :id "comment"
191 | :class ["input_active" "input_error"]
192 | :style { :background-color "#EEE"
193 | :margin-left 42 }
194 | :on-change (fn [e]
195 | (js/alert (.. e -target -value))) }]
196 | ```
197 |
198 | `` is a zero, one or many elements (strings or nested tags) with the same syntax:
199 |
200 | ```clojure
201 | [:div {} "Text"] ;; tag, attrs, nested text
202 | [:div {} [:span]] ;; tag, attrs, nested tag
203 | [:div "Text"] ;; omitted attrs
204 | [:div "A" [:em "B"] "C"] ;; 3 children, mix of text and tags
205 | ```
206 |
207 | Children can include lists or sequences which will be flattened:
208 |
209 | ```clojure
210 | [:div (list [:i "A"] [:b "B"])] === [:div [:i "A"] [:b "B"]]
211 | ```
212 |
213 | By default all text nodes are escaped. To embed an unescaped string into a tag, add the `:dangerouslySetInnerHTML` attribute and omit children:
214 |
215 | ```clojure
216 | [:div { :dangerouslySetInnerHTML {:__html ""}}]
217 | ```
218 |
219 | ### Rendering component
220 |
221 | Given this code:
222 |
223 | ```clojure
224 | (require [rum.core :as rum])
225 |
226 | (rum/defc repeat-label [n text]
227 | [:div (repeat n [:.label text])])
228 | ```
229 |
230 | First, we need to create a component instance by calling its function:
231 |
232 | ```
233 | (repeat-label 5 "abc")
234 | ```
235 |
236 | Then we need to pass that instance to `(rum.core/mount comp dom-node)`:
237 |
238 | ```clojure
239 | (rum/mount (repeat-label 5 "abc") js/document.body)
240 | ```
241 |
242 | And we will get this result:
243 |
244 | ```html
245 |
246 |