~benaiah/fennel-openresty

cec36a2598f92156d8367f47c6144a9f0f9b72ca — Benaiah Mischenko 2 years ago 992db6c
Recreate vanilla JS toy websocket client in React and Fennel
5 files changed, 104 insertions(+), 14 deletions(-)

M app/makefile
M app/package.json
M app/src/app.fnl
A app/src/react-helpers.fnl
M server/server.fnl
M app/makefile => app/makefile +3 -1
@@ 7,7 7,9 @@ clean:
	rm -f src/*.lua
	rm -rf node_modules

dist/main.js: src/app.lua src/index.js
dist/main.js: src/react-helpers.lua src/app.lua src/index.js
	yarn && yarn run build
src/app.lua: src/app.fnl
	modules/fennel/fennel --compile $^ > $@
src/react-helpers.lua: src/react-helpers.fnl
	modules/fennel/fennel --compile $^ > $@

M app/package.json => app/package.json +1 -1
@@ 5,7 5,7 @@
  "author": "Benaiah Mischenko",
  "license": "AGPL",
  "scripts": {
    "build": "webpack --config webpack.config.js"
    "build": "webpack --mode development --config webpack.config.js"
  },
  "devDependencies": {
    "fengari-loader": "^0.0.1",

M app/src/app.fnl => app/src/app.fnl +74 -3
@@ 3,8 3,79 @@
(local React (require "react"))
(local ReactDOM (require "react-dom"))

(local {:use-state use-state
        :use-ref use-ref
        :create-context create-context
        :use-context use-context
        :get-children-as-array get-children-as-array}
       (require "./react-helpers.lua"))

(component! LogLine [{:children message}]
  (c! :div {:className :log-message} message))

(component! Log [{:children maybe-children}]
  (let [children (get-children-as-array maybe-children)]
    (: children :map (fn [_ child i] (c! LogLine {:key i} child)))))

(component! WebSocketDebugForm
  [{:connect connect :disconnect disconnect :on-message on-message}]
  (let [(current-message set-current-message) (use-state "")]
    (c! [:div {}
         [:form {:onSubmit (fn []
                             (on-message current-message)
                             (set-current-message ""))}
          [:button {:type :button :onClick connect} :Connect]
          [:button {:type :button :onClick disconnect} :Disconnect]
          [:input {:id :text
                   :type :text
                   :value current-message
                   :onChange (fn [_ e] (set-current-message e.target.value))}]
          [:button {:type :submit} :Send]]])))

(fn create-websocket
  [{:ws-url ws-url
    :on-open onopen
    :on-close onclose
    :on-error onerror
    :on-message onmessage}]
  (let [ws (js.new js.global.WebSocket ws-url)]
    (set ws.onopen onopen)
    (set ws.onerror onerror)
    (set ws.onmessage onmessage)
    (set ws.onclose onclose)
    ws))

(local ws-url "ws://localhost:8090/wss")
(component! App []
  (let [(log-messages set-log-messages) (use-state (js! []))
        (ws set-ws) (use-state nil)
        log (fn [message]
              (: js.global.console :log message)
              (set-log-messages
               (fn [_ prev-messages] (: prev-messages :concat message))))
        ws-on-open (fn [] (log "connected"))
        ws-on-error (fn [_ err] (log err))
        ws-on-message (fn [_ e] (log (.. "recv: " e.data)))
        ws-on-close (fn [] (log "disconnected") (set-ws nil))
        connect
        (fn []
          (if ws (log "already connected")
              (set-ws (create-websocket
                       {:ws-url ws-url
                        :on-open ws-on-open
                        :on-error ws-on-error
                        :on-message ws-on-message
                        :on-close ws-on-close}))))
        disconnect (fn [] (if ws (log "already disconnected") (: ws :close)))
        send-message (fn [message]
                       (if (not ws) (log "please connect first")
                           (log (.. "send: " message) (: ws :send message))))]
    (c! [React.Fragment {} 
         [WebSocketDebugForm {:connect connect
                              :disconnect disconnect
                              :on-message send-message}]
         [Log {} log-messages]])))

(: ReactDOM :render
   (c! [:h1 {} "Hello from React!"])
   (c! App {})
   (: js.global.document :getElementById :react-root))

(print "Hello world!")

A app/src/react-helpers.fnl => app/src/react-helpers.fnl +26 -0
@@ 0,0 1,26 @@
(local React (require "react"))

(fn use-state [initial]
  (let [val (React.useState nil initial)
        state (. val 0)
        set-state (. val 1)]
    (values state (fn [new-state] (set-state nil new-state)))))

(fn use-ref [initial] (React.useRef nil initial))

(fn create-context [initial] (React.createContext nil initial))
(fn use-context [context] (React.useContext nil context))

(fn use-effect [effect dependencies]
  (React.useEffect nil effect dependencies))

(fn get-children-as-array [maybe-children]
  (if (: js.global.Array :isArray maybe-children)
      maybe-children
      (js! [maybe-children])))

{:use-state use-state
 :use-ref use-ref
 :create-context create-context
 :use-context use-context
 :get-children-as-array get-children-as-array}

M server/server.fnl => server/server.fnl +0 -9
@@ 1,8 1,6 @@
(global ngx ngx)
(local html (require :server.html))

(local script "var ws = null; function connect() { if (ws !== null) return log('already connected'); ws = new WebSocket('ws://127.0.0.1:8090/wss/'); ws.onopen = function () { log('connected'); }; ws.onerror = function (error) { log(error); }; ws.onmessage = function (e) { log('recv: ' + e.data); }; ws.onclose = function () { log('disconnected'); ws = null; }; return false;}function disconnect() { if (ws === null) return log('already disconnected'); ws.close(); return false;}function send() { if (ws === null) return log('please connect first'); var text = document.getElementById('text').value; document.getElementById('text').value = ''; log('send: ' + text); ws.send(text); return false;}function log(text) { var li = document.createElement('li'); li.appendChild(document.createTextNode(text)); document.getElementById('log').appendChild(li); return false;}")

(local homepage
       [:html {}
        [:head {}


@@ 10,13 8,6 @@
         [:script {} script]]
        [:body {}
         [:div {:id :react-root}]
         [:div {}
          [:form {:onSubmit "return send();"}
           [:button {:type :button :onClick "return connect();"} :Connect]
           [:button {:type :button :onClick "return disconnect();"} :Disconnect]
           [:input {:id :text :type :text}]
           [:button {:type :submit} :Send]]]
         [:ol {:id :log}]
         [:script {:src "app.js"}]]])

(ngx.say (.. "<!doctype html>\n" (html homepage)))