How is a chat input field defined in reagent? - textarea

Say that you have a text field which is the input of a chat program written in cljs with reagent. It could look something like this:
(defn chat-input []
(let [written-text (atom "")]
(fn []
[:textarea
{:value #written-text
:on-change #(reset! written-text (-> % .-target .-value))}])))
Now the easy way to implement sending a message is to add a send button. But there's one interaction that's so integral to chat that you can't really be without it: That enter or shift-enter sends the message. But I can't figure out how to implement it.
My first try was to simply add a :on-key-press event handler to send the message and reset the state to "". This solution was inspired by How to detect enter key press in reagent.
(defn chat-input []
(let [written-text (atom "")]
(fn []
[:textarea
{:value #written-text
:on-change #(reset! written-text (-> % .-target .-value))
:on-key-press (fn [e]
(let [enter 13]
(println "Key press" (.-charCode e))
(if (= (.-charCode e) enter)
(reset! written-text "")
(println "Not enter."))))}])))
The problem being that the call to (reset! written-text "") in :on-key-press has no effect, probably because it's overridden by the :on-change event handler.
So do you have any ideas on how to implement this functionality? If so, please do share!

you were on the right track, but forgot about the js event model: in your case both onChange and onKeyPress are triggered, because the target is a textarea where enter key changes the input. So in js onKeyPress is triggered first, and then it triggers onChange if the key would change something. What you need is to disable this default behavior of keyPress with preventDefault:
(defn chat-input []
(let [written-text (atom "")]
(fn []
[:textarea
{:value #written-text
:on-change #(reset! written-text (.. % -target -value))
:on-key-press (fn [e]
(when (= (.-charCode e) 13)
(.preventDefault e)
(reset! written-text "")))}])))
that should fix the problem.

Here's lot more advanced solution that mccraigmccraig on the clojurians slack so kindly allowed me to share with you. It expands the height of the textarea as the contents of the input gets larger which emulates how the chat input works in slack.
But the important part for this question is that it's :on-key-press contains a (.preventDefault e).
(defn update-rows
[row-count-atom max-rows dom-node value]
(let [field-height (.-clientHeight dom-node)
content-height (.-scrollHeight dom-node)]
(cond
(and (not-empty value)
(> content-height field-height)
(< #row-count-atom max-rows))
(swap! row-count-atom inc)
(empty? value)
(reset! row-count-atom 1))))
(defn expanding-textarea
"a textarea which expands up to max-rows as it's content expands"
[{:keys [max-rows] :as opts}]
(let [dom-node (atom nil)
row-count (atom 1)
written-text (atom "")
enter-keycode 13]
(reagent/create-class
{:display-name "expanding-textarea"
:component-did-mount
(fn [ref]
(reset! dom-node (reagent/dom-node ref))
(update-rows row-count max-rows #dom-node #written-text))
:component-did-update
(fn []
(update-rows row-count max-rows #dom-node #written-text))
:reagent-render
(fn [{:keys [on-change-fn] :as opts}]
(let [opts (dissoc opts :max-rows)]
[:textarea
(merge opts
{:rows #row-count
:value #written-text
:on-change (fn [e]
(reset! written-text (-> e .-target .-value)))
:on-key-down (fn [e]
(let [key-code (.-keyCode e)]
(when (and (= enter-keycode key-code)
(not (.-shiftKey e))
(not (.-altKey e))
(not (.-ctrlKey e))
(not (.-metaKey e)))
(do
(.preventDefault e)
(send-chat! #written-text)
(reset! written-text "")))))})]))})))

Related

Why Dafny complains about this post condition "ensures ListMap(x => x + 1, Cons(1, Cons(2, Nil))) == Cons(2, Cons(3, Nil))", and how can it be fixed?

function method ListMap<T,X>(f : (T -> X), l : List<T>) : List<X>
ensures ListMap(x => x + 1, Cons(1, Cons(2, Nil))) == Cons(2, Cons(3, Nil))
{
match l {
case Nil => Nil
case Cons(n, l') => Cons(f(n), ListMap(f, l'))
}
}
Dafny raises two complaint here.
about "case Nil": A postcondition might not hold on this return path.
about "ensure...": This postcondition might not hold on a return path.
This snippet is from the book "Introducing Software Verification with Dafny Language: Proving Program Correctness", but I can't find the Errata for it.
There are two things here that you can solve at once:
The ensures won't terminate if computed (in an imaginary ghost environment) because you provide it as an intrinsic postcondition, so you will get into trouble.
Dafny is ok to ensure something about the output of the current function, but for everything other call to the function itself, it has to prove termination.
What you provide is an example of a postcondition, not a fact that ought to be known by every user of your ListMap function.
The solution is to refactor your ensures in a lemma:
datatype List<T> = Nil | Cons(t: T, tail: List<T>)
function method ListMap<T,X>(f : (T -> X), l : List<T>) : List<X>
{
match l {
case Nil => Nil
case Cons(n, l') => Cons(f(n), ListMap(f, l'))
}
}
lemma ListMapExample()
ensures ListMap(x => x + 1, Cons(1, Cons(2, Nil))) == Cons(2, Cons(3, Nil))
{
}

File Upload using Fable-Elmish

I want to upload a file to my Fable-Elmish, end so that I can then send it to the server for processing. However, I can't find any documentation / samples to cover this. This is my update function:
let update msg model : Model * Cmd<Msg> =
match msg with
| QueryResults ->
{model with results = None}, Cmd.ofPromise getData "" FetchSuccess FetchFailure
| FetchSuccess data ->
{ model with results = Some data }, []
| FetchFailure ex ->
Browser.console.log (unbox ex.Message)
Browser.console.log "exception occured" |> ignore
model, []
| FileUploaded ->
Browser.console.log "file selected!" |> ignore
model, []
And this is the part of the view function containing the file upload:
R.input [
Type "file"
OnChange (fun x -> FileUploaded |> ignore)
] []
As far as I can tell, this should trigger the update and print out "file uploaded!" to the console, but nothing is happening.
If anyone could point me in the right direction here that would be great.
You're passing the FileUploaded message to ignore, which does just what its name says: ignore its arguments and do nothing. So that message won't actually go anywhere.
With Fable-Elmish, your view function takes an argument called dispatch, which is a function that will take a message and put it into the message queue (so that update will receive the message at some later time). Look at the TodoMVC sample, and especially the onEnter and viewModel functions, for details.
Basically, your OnChange (fun x -> FileUploaded |> ignore) line should have been OnChange (fun x -> FileUploaded |> dispatch) instead.

F# - Sanity Checks and Options

I'm pretty new to F# so it's hard for me to change my mindset after many years of C#/Java OOP.
I have an event handler MyForm.SelectFile(filePath:String) that opens a dialog and let you select the file to read. Once the file is selected, Parser.LoadFile(filePath:String) is called:
static member LoadFile(filePath:String) =
if not <| ZipFile.IsZipFile(filePath) then
failwith "invalid file specified."
use zipFile = new ZipFile(filePath)
if zipFile.Count <> 2 || zipFile |> Seq.exists(fun x -> x.FileName <> "alpha" && x.FileName <> "beta") then
failwith "invalid file specified."
zipFile |> fun x -> Parser.Parse(x.OpenReader())
I'm always expecting the selected file to be a valid zip archive containing 2 files without extension: "alpha" and "beta".
First, is there a better way to sanitize my input?
My if statements are pretty long and I'm sure F# can provide better solutions, but I really can't figure out.
Second, using failwith is forcing me to handle exceptions in my MyForm.SelectFile(filePath:String) method and I think Options could be a better solution.
I can't figure out how to use them if I need to perform two different and consecutive checks (ZipFile.IsZipFile and content) because in between I have to instantiate a ZipFile.
In C# I would just return null whenever a check fails and then checking the return value against null would let me know whether I need to prompt an error or continue.
Current code:
type Parser with
static member isValidZipFile (zipFile:ZipFile) =
(zipFile.Count = 2) && (zipFile |> Seq.forall(fun x -> (x.FileName = "alpha") || (x.FileName = "beta")))
static member LoadFile(filePath:String) =
if not <| ZipFile.IsZipFile(filePath) then
None
else
use zipFile = new ZipFile(filePath)
if not <| Parser.isValidZipFile(zipFile) then
None
else
Some(seq { for zipEntry in zipFile do yield Parser.Parse(zipEntry.OpenReader()) } |> Seq.toArray)
First, the last line of your function could be a bit more elegant if it was written like:
zipFile.OpenReader() |> Parser.Parse
Second, you're on the right track as far as your thinking about using Option. It's really pretty simple in this case:
static member LoadFile(filePath:String) =
if not <| ZipFile.IsZipFile(filePath) then None else
use zipFile = new ZipFile(filePath)
if zipFile.Count <> 2 || zipFile |> Seq.exists(fun x -> x.FileName <> "alpha" && x.FileName <> "beta") then None else
Some (zipFile.OpenReader() |> Parser.Parse)
That last line could also be written as:
zipFile.OpenReader() |> Parser.Parse |> Some
Now, you mentioned that you don't like the long if statement. Let's turn it into a function! And I usually prefer functions with "positive" names, i.e. an isValidInput function is usually more helpful than an isInvalidInput. So let's make a function that checks if a zipfile is actually valid:
let isValid (z:ZipFile) =
z.Count = 2 && z |> Seq.forAll(fun x -> x.FileName = "alpha" || x.FileName = "beta")
Now your LoadFile function can become:
static member LoadFile(filePath:String) =
if not <| ZipFile.IsZipFile(filePath) then None else
use zipFile = new ZipFile(filePath)
if not <| isValid zipFile then None else
zipFile.OpenReader() |> Parser.Parse |> Some
And that looks pretty easy to read, so we can stop refactoring for now.
This piece of code looks weird. Using Sequence expressions for such a simple piece of code is overkill.
Some(seq { for zipEntry in zipFile do yield Parser.Parse(zipEntry.OpenReader()) } |> Seq.toArray)
You could write it better like this
zipFile |> Seq.map (fun ze -> ze.OpenReader () |> Parser.parse) |> Some
Or if you insist in doing it in an array (why?)
zipFile |> Seq.map (fun ze -> ze.OpenReader () |> Parser.parse) |> Seq.toArray |> Some
You'll end up with type signature option<seq<value>>. I am not sure if this is a good idea, but it is not possible to tell without looking at the rest of your code.

ClojureScript - construct url conditionally in go block

I am using cemerick/url to construct my URL for an ajax request.
However, some of the parameters of the query are coming from an asynchronous native callback. So I put everything in a go block, like the following :
(defn myFn [options]
(go (let [options (cond-> options
;; the asynchronous call
(= (:loc options) "getgeo") (assoc :loc (aget (<! (!geolocation)) "coords")))
constructing the url
url (-> "http://example.com"
(assoc-in [:query :param] "a param")
;; How do I not associate those if they don't exist ?
;; I tried something along the lines of this, but it obviously doesn't work.
;; (cond-> (and
;; (-> options :loc :latitude not-nil?)
;; (-> options :loc :latitude not-nil?))
;; (do
;; ))
;; these will fail if there is no "latitude" or "longitude" in options
(assoc-in [:query :lat] (aget (:loc options) "latitude"))
(assoc-in [:query :lng] (aget (:loc options) "longitude"))
;; url function from https://github.com/cemerick/url
(url "/subscribe")
str)])))
I would like to be able to be able to pass either {:loc "local} or {:loc {:latitude 12 :longitude 34}} or {} as a parameter to my function.
I feel that I am not using the right structure already.
How I should construct this url ?
If I understood your question right, you require code, which looks like that:
;; helper function
(defn add-loc-to-url-as [url loc k as-k]
(if-let [v (k loc)]
(assoc-in url [:query as-k] v)
url))
;; define function, that process options and extends URL
(defn add-location [url options]
(if-let [location (:loc options)]
(if (string? location)
;;; it expects string, for {:loc "San Francisco"}
(assoc-in url [:query :location] location)
;;; or map, for {:loc {:latitude 12.123, :longitude 34.33}}
(-> url
(add-loc-to-url-as location :latitude :lat)
(add-loc-to-url-as location :longitude :lng)))
url))
;; now you can use it in your block
;; ...
the-url (-> (url "http://example.com" "subscribe")
(assoc-in [:query :param] "a param")
(add-location options))

Can't get the post in LISP hunchentoot

I try to implement a simple post example based on Hunchentoot.
Here is the code:
(define-easy-handler (test :uri "/test") ()
(with-html-output-to-string (*standard-output* nil :prologue t :indent t)
(:html
(:body
(:h1 "Test")
(:form :action "/test2" :method "post" :id "addform"
(:input :type "text" :name "name" :class "txt")
(:input :type "submit" :class "btn" :value "Submit"))))))
(define-easy-handler (test2 :uri "/test2") (name)
(with-html-output-to-string (*standard-output* nil :prologue t :indent t)
(:html
(:body
(:h1 name)))))
I can correctly connect to http://127.0.0.1:8080/test and see the text input form. But when I submit the text, I get a blank page where I expected a page with the title given in text input.
Not sure what is wrong, can anyone advice?
Change your handler to this
(define-easy-handler (test2 :uri "/test2") (name)
(with-html-output-to-string (*standard-output* nil :prologue t :indent t)
(:html
(:body
(:h1 (str name))))))
Then it should work. Read the cl-who documentation.
Especially the information on local macros.
I am including the relevant documentation here.
A form which is neither a string nor a keyword nor a list beginning with a keyword will be left as is except for the following local macros:
Forms that look like (str form) will be substituted with
(let ((result form)) (when result (princ result s)))
(loop for i below 10 do (str i)) =>
(loop for i below 10 do
(let ((#:result i))
(when #:result (princ #:result *standard-output*))))
Forms that look like (fmt form*) will be substituted with
(format s form*)
(loop for i below 10 do (fmt "~R" i)) => (loop for i below 10 do (format s "~R" i))
Forms that look like (esc form) will be substituted with
(let ((result form)) (when result (write-string (escape-string result s))))
If a form looks like (htm form*) then each of the forms will be subject to the transformation rules we're just describing, i.e. this is the body is wrapped with another invocation of WITH-HTML-OUTPUT.
(loop for i below 100 do (htm (:b "foo") :br))
=> (loop for i below 100 do (progn (write-string "<b>foo</b><br />" s)))

Resources