Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 84 additions & 69 deletions src/joy/form-helper.janet
Original file line number Diff line number Diff line change
Expand Up @@ -2,110 +2,118 @@
(import ./helper :prefix "")
(import ./csrf :prefix "")

(defn- field [kind val key & attrs]
[:input (merge {:type kind :name (string key) :value (get val key)} (table ;attrs))])
(defn- field [kind name & attrs]
[:input (merge {:type kind :name (string name)} (struct ;attrs))])


(def hidden-field
`(hidden-field val key & attrs)
(defn hidden-field
`(hidden-field name & attrs)

Generates an <input type="hidden" /> html element where
val is a dictionary and key is the value html attribute of a key
in the val dictionary. If key is nil, an error will be thrown.
name is a keyword denoting the name html attribute.

Ex.

(hidden-field {:a "a" :b "b"} :a :class "a-class" :style "a-style")
(hidden-field {:a "a" :b "b"} :b)`
(partial field "hidden"))
(hidden-field :myhiddenfield :value "hiddenvalue" :class "a-class" :style "a-style")
(hidden-field :api-token :value "secret-token")
(hidden-field :valueless-hidden-field)`
[name & attrs]
(field "hidden" name ;attrs))


(def text-field
`(text-field val key & attrs)
(defn text-field
`(text-field name & attrs)

Generates an <input type="text" /> html element where
val is a dictionary and key is the value html attribute of a key
in the val dictionary. If key is nil, an error will be thrown.
name is a keyword denoting the name html attribute.

Ex.

(text-field {:a "a" :b "b"} :a :class "a-class" :style "a-style")
(text-field {:a "a" :b "b"} :b)`
(partial field "text"))
(text-field :username :placeholder "Enter Username" :class "a-class" :style "a-style")
(text-field :some-prefilled-text :value "I am prefilled!")
(text-field :text-field)`
[name & attrs]
(field "text" name ;attrs))


(def email-field
`(email-field val key & attrs)
(defn email-field
`(email-field name & attrs)

Generates an <input type="email" /> html element where
val is a dictionary and key is the value html attribute of a key
in the val dictionary. If key is nil, an error will be thrown.
name is a keyword denoting the name html attribute.

Ex.

(email-field {:a "a" :b "b"} :a :class "a-class" :style "a-style")
(email-field {:a "a" :b "b"} :b)`
(partial field "email"))
(email-field :email-address :placeholder "Email" :value "[email protected]")
(email-field :email :class "a-class" :style "a-style")
(email-field :email)`
[name & attrs]
(field "email" name ;attrs))


(def password-field
`(password-field val key & attrs)
(defn password-field
`(password-field name & attrs)

Generates an <input type="password" /> html element where
val is a dictionary and key is the value html attribute of a key
in the val dictionary. If key is nil, an error will be thrown.
name is a keyword denoting the name html attribute.

Ex.

(password-field {:a "a" :b "b"} :a :class "a-class" :style "a-style")
(password-field {:a "a" :b "b"} :b)`
(partial field "password"))
(password-field :pass-field :placeholder "Password" :class "a-class" :style "a-style")
(password-field :pswd :class "a-class" :style "a-style")
(password-field :pswd)`
[name & attrs]
(field "password" name ;attrs))


(def file-field
`(file-field val key & attrs)
(defn file-field
`(file-field name & attrs)

Generates an <input type="file" /> html element where
val is a dictionary and key is the value html attribute of a key
in the val dictionary. If key is nil, an error will be thrown.
name is a keyword denoting the name html attribute.

Ex.

(file-field {:a "a" :b "b"} :a :class "a-class" :style "a-style")
(file-field {:a "a" :b "b"} :b)`
(partial field "file"))
(file-field :file-field :accept "image/*,.pdf")
(file-field :file-field :class "a-class" :style "a-style")
(file-field :file-field)`
[name & attrs]
(field "file" name ;attrs))


(defn checkbox-field
`(checkbox-field val key & attrs)
`(checkbox-field name checked? & attrs)

Generates two inputs, one hidden and one checkbox
where val is a dictionary and key is the value html attribute of a key
in that val dictionary. The first checkbox input is hidden
where name is a keyword denoting the name html attribute,
and checked? is a boolean denoting whether the checkbox is
checked by default.

Ex.

(checkbox-field {:enabled true} :enabled :class "a-class" :style "a-style")
(checkbox-field :neovim? true :true "you're cool" :false "reconsider")
(checkbox-field :something false :class "a-class" :style "a-style")

=>

<input type="hidden" name="enabled" value="0" class="a-class" style="a-style" />
<input type="checkbox" name="enabled" value="1" checked="" class="a-class" style="a-style" />`
[val key & attrs]
(let [checked (if (or (true? (get val key))
(one? (get val key)))
{:checked ""}
{})
<input type="hidden" name="neovim?" value="reconsider" />
<input type="checkbox" name="enabled" value="you're cool" checked="" />
<input type="hidden" name="something" value="0" class="a-class" style="a-style" />
<input type="checkbox" name="something" value="1" class="a-class" style="a-style" />`
[name checked? & attrs]
(let [checked (if checked? {:checked ""} {})
attrs (struct ;attrs)]

[[:input {:type "hidden" :name key :value (get attrs :false 0)}]
[:input (merge {:type "checkbox" :name key :value (get attrs :true 1)}
[(hidden-field name :value (get attrs :false 0))
[:input (merge {:type "checkbox" :name (string name) :value (get attrs :true 1)}
checked
attrs)]]))


(defn form-for
`Generates a <form> html element where action-args is a tuple
`(form-for action-args & body)

Generates a <form> html element where action-args is a tuple
of [request route-keyword route-arg1 route-arg2...] and
body is the rest of the form. The form requires the request for
the csrf-token and any put, patch or delete http methods.
Expand All @@ -115,21 +123,22 @@

(form-for [request :account/patch {:id 1}]
(label :name "Account name")
(text-field {:name "name"} :name)
(text-field :name)
(submit "Save name"))`
[action-args & body]
(let [[request] action-args
action (apply router/action-for (drop 1 action-args))]
action (apply router/action-for (drop 1 action-args))
_method (action :_method)]
[:form action
body
(csrf-field request)
(when (truthy? (action :_method))
(hidden-field action :_method))]))
(when (truthy? _method)
(hidden-field :_method :value _method))]))


(defn form-with
[request &opt options & body]
`
`(form-with request &opt options & body)

Generates an html <form> element where the request is the request dictionary and options
are any form options.

Expand All @@ -146,30 +155,34 @@

(form-with request {:route :account/new :enctype "multipart/form-data"}
(label :name "name")
(file-field {} :name)
(file-field :name)
(submit "Upload file"))

(form-with request (merge (action-for :account/edit {:id 1}) {:enctype "multipart/form-data"})
(label :name "name")
(file-field {} :name)
(file-field :name)
(submit "Upload file"))`
[request &opt options & body]
(default options {})
(let [{:action action :route route} options
action (if (truthy? action)
{:action action}
(if (truthy? route)
(router/action-for ;(if (indexed? route) route [route]))
{:action ""}))
attrs (merge options action)]
attrs (merge options action)
_method (get attrs :_method)]
[:form attrs
body
(csrf-field request)
(when (truthy? (get attrs :_method))
(hidden-field attrs :_method))]))
(when (truthy? _method)
(hidden-field :_method :value _method))]))


(defn label
`Generates a <label> html element where html-for
`(label html-for body & attrs)

Generates a <label> html element where html-for
is the for attribute value (as a keyword) and the
body is usually just the label's string value, args
represents the rest of the attributes, if any.
Expand All @@ -178,8 +191,8 @@

(label :name "Account name")
(label :name "Account name" :class "form-label")`
[html-for body & args]
[:label (merge {:for (string html-for)} (table ;args))
[html-for body & attrs]
[:label (merge {:for (string html-for)} (table ;attrs))
body])


Expand All @@ -190,13 +203,15 @@


(defn submit
`Generates an <input type="submit" /> html element
`(submit value & attrs)

Generates an <input type="submit" /> html element
where value is the value attribute and the args
are any html attributes.

Ex.

(submit "Save")
(submit "Save" :class "btn btn-submit")`
[value & args]
[:input (merge {:type "submit" :value value} (table ;args))])
[value & attrs]
[:input (merge {:type "submit" :value value} (table ;attrs))])
45 changes: 38 additions & 7 deletions test/joy/form-helper-test.janet
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
(defn hello [request])
(defn hello1 [request])
(defn hello2 [request])
(def req {:masked-token "masked csrf token"})


(deftest
Expand All @@ -32,14 +33,44 @@
[:input @{:type "hidden" :value :patch :name "_method"}]]
(form-with {} (action-for :hello2 {:id 2}))))

(test "checkbox-field"
(is (deep= [[:input {:type "hidden" :name :finished :value 0}]
[:input @{:type "checkbox" :name :finished :value 1 :checked ""}]]
(test "form-for"
(is (deep= [:form {:method :post :_method :patch :action "/accounts/3"}
[[:label @{:for "name"} "Account name"]
[:input @{:type "text" :name "name"}]
[:input @{:type "submit" :value "Save name"}]]
[:input {:type "hidden" :name "__csrf-token" :value "masked csrf token"}]
[:input @{:type "hidden" :name "_method" :value :patch}]]
(form-for [req :hello2 {:id 3}]
(label :name "Account name")
(text-field :name)
(submit "Save name")))))

(test "hidden-field"
(is (deep= [:input @{:type "hidden" :name "hiddenfield" :value "hiddenvalue"}]
(hidden-field :hiddenfield :value "hiddenvalue"))))

(test "text-field"
(is (deep= [:input @{:type "text" :name "text-field" :placeholder "Enter text" :class "a-class"}]
(text-field :text-field :placeholder "Enter text" :class "a-class"))))

(test "email-field"
(is (deep= [:input @{:type "email" :name "email-field" :placeholder "Enter email" :class "a-class"}]
(email-field :email-field :placeholder "Enter email" :class "a-class"))))

(test "password-field"
(is (deep= [:input @{:type "password" :name "password-field" :placeholder "Enter password" :class "a-class"}]
(password-field :password-field :placeholder "Enter password" :class "a-class"))))

(checkbox-field {:finished 1} :finished))))
(test "file-field"
(is (deep= [:input @{:type "file" :name "file-field" :accept "image/*,.pdf" :class "a-class"}]
(file-field :file-field :accept "image/*,.pdf" :class "a-class"))))

(test "checkbox-field"
(is (deep= [[:input {:type "hidden" :name :finished :value 0}]
[:input @{:type "checkbox" :name :finished :value 1 :checked "" :class "class1 class2"}]]
(is (deep= [[:input @{:type "hidden" :name "finished" :value 0}]
[:input @{:type "checkbox" :name "finished" :value 1 :checked ""}]]
(checkbox-field :finished true))))

(checkbox-field {:finished 1} :finished :class "class1 class2")))))
(test "checkbox-field 2"
(is (deep= [[:input @{:type "hidden" :name "finished" :value 0}]
[:input @{:type "checkbox" :name "finished" :value 1 :class "class1 class2"}]]
(checkbox-field :finished false :class "class1 class2")))))