Skip to content

Instantly share code, notes, and snippets.

@soegaard
Created September 23, 2020 17:30
Show Gist options
  • Save soegaard/0622c798426f2aef168b6d4dc6568ca5 to your computer and use it in GitHub Desktop.
Save soegaard/0622c798426f2aef168b6d4dc6568ca5 to your computer and use it in GitHub Desktop.
#lang at-exp racket/base
(require racket/runtime-path racket/format racket/file
urlang urlang/html urlang/extra urlang/react/urx urlang/for
syntax/parse racket/syntax)
;;;
;;; CONFIGURATION
;;;
(current-urlang-run? #f) ; run using Node?
(current-urlang-echo? #t) ; print generated JavaScript?
(current-urlang-console.log-module-level-expr? #f) ; print top-level expression?
(current-urlang-beautify? #t) ; invoke js-beautify
;;;
;;; REACT HOOKS
;;;
; SYNTAX
; (use-state id set-id initial-expression)
; is equivalent to the following JavaScript:
; const [id, set-id] = useState(initial-expression);
; in other words:
; a state variable is initialized with the value of initial-expresion,
; id can be used to reference the value, and set-id to set it.
(define-urlang-macro use-state
(λ (stx)
(syntax-parse stx
[(_ state-id set-state-id initial-expr)
(syntax/loc stx
(var [state-temp (React.useState initial-expr)]
[state-id (ref state-temp 0)]
[set-state-id (ref state-temp 1)]))])))
;;;
;;; GENERAL UTILITIES
;;;
; SYNTAX
; (def name expr)
; (def (name arg ...) . body)
; expands to
; (var [name expr])
; and (var [name (λ (arg ...) . body)])
; respectively.
(define-urlang-macro def
(λ (stx)
(syntax-parse stx
[(_def (name arg ...) . body)
(syntax/loc stx
(var [name (λ (arg ...) . body)]))]
[(_def name expr)
(syntax/loc stx
(var [name expr]))])))
; SYNTAX
; (try-expr expr1 catch-expr)
; Evaluate expr1 and return the result.
; If an expception is thrown during the evaluation of expr1,
; then pass the exception to the result of catch-expr,
; which is expected to be a function of one variable.
(define-urlang-macro try-expr
(λ (stx)
(syntax-parse stx
[(_try-expr expr1 catch-expr)
; catch-expr evaluates to a function of 1 argument
(syntax/loc stx
((λ ()
(var [try-result #f])
(try {(:= try-result expr1)}
(catch exn
; (console.log "inside catch")
(:= try-result (catch-expr exn))))
try-result)))])))
;;;
;;; REACT BOOTSTRAP
;;;
; This example uses the React version of Bootstrap:
; https://react-bootstrap.github.io/
; When the React Bootstrap project is imported, the object ReactBootstrap contains the components.
; The components can then be used as `ReactBootrap.Form`, `ReactBootrap.Button` etc.
; But I think it is simpler to write just `Form`, `Button` etc, so a little macro is used
; to bring the components into scope.
; SYNTAX
; (import-from-react-bootstrap id ...)
; Import id ... from the ReactBootstrap object.
(define-urlang-macro import-from-react-bootstrap
(λ (stx)
(syntax-parse stx
[(_ id ...)
(syntax/loc stx
(var [id (dot ReactBootstrap id)] ...))])))
;;;
;;; react-example.js
;;;
; The result of compiling the Urlang module below is saved in "react-example.js".
(define-runtime-path react-example.js "react-example.js")
(parameterize ([current-urlang-output-file react-example.js])
(urlang
(urmodule exercise ; saved in exercise.js
(import delete this Promise fetch Object $ React ReactDOM document.title ReactBootstrap JSON RegExp)
(import-from-react-bootstrap Button ButtonGroup ButtonToolbar Card CardGroup
Container Col Row Form InputGroup ListGroup Modal Overlay Popover)
;;; Import from React Hooks.
(var [use-effect React.useEffect]
[use-ref React.useRef])
;;;
;;; The App Component
;;;
; This example App component will
; - have a header
; - display a counter
; - have a button, that when clicked, will increment the counter
; React Component pass initialization arguments through props.
; In this example, we will pass the title displayed on top as a prop.
(define (App1 props)
;; State
(use-state count set-count 0) ; The output depends on the state of the counter
;; Event handlers
(def (on-button-click)
(set-count (+ count 1)))
;; Each time the state changes, the component must be rerendered.
;; An `urx` expression (equivalent to a jsx-expression) dynamically
;; creates a dom-element. Use small case names such as div, span, h1
;; for standard html-tags and upper-case for React components.
@urx[@div{@h1{React Example}
@p{You have clicked @ur[count] times at the button.}
@button[onClick: @ur[on-button-click]]{Click me}}])
; In App2 we introduce Bootstrap React.
; Instead of a plain button, we will use the Bootstrap component Button.
(define (App2 props)
;; State
(use-state count set-count 0) ; The output depends on the state of the counter
;; Event handlers
(def (on-button-click)
(set-count (+ count 1)))
;; Each time the state changes, the component must be rerendered.
;; An `urx` expression (equivalent to a jsx-expression) dynamically
;; creates a dom-element. Use small case names such as div, span, h1
;; for standard html-tags and upper-case for React components.
@urx[@Container[
@h1{React Example}
@Card[bg: "success"
style: @ur[(object [width "20rem"])]]{
@Card.Body[
@Card.Title{The Click Counter}
@Card.Text{You have clicked @ur[count] times at the button.}]}
@br[]
@Button[onClick: @ur[on-button-click]]{Click me}]])
; In App3 we introduce an effect to update the title.
(define (App3 props)
;; State
(use-state count set-count 0) ; The output depends on the state of the counter
(use-state title-needs-an-update? set-title-needs-an-update? #t) ; Flag: update title?
;; Effects
;; Note: Make sure effects, don't trigger a rerender.
; Here we use `title-needs-an-update?` to avoid that.
(use-effect (λ () (when title-needs-an-update?
(:= document.title (+ "You clicked " count " times"))
(set-title-needs-an-update? #f)))
(array count))
;; Event handlers
(def (on-button-click)
(set-count (+ count 1))
(set-title-needs-an-update? #t))
;; Each time the state changes, the component must be rerendered.
;; An `urx` expression (equivalent to a jsx-expression) dynamically
;; creates a dom-element. Use small case names such as div, span, h1
;; for standard html-tags and upper-case for React components.
@urx[@Container[
@h1{React Example}
@Card[bg: "success"
style: @ur[(object [width "20rem"])]]{
@Card.Body[
@Card.Title{The Click Counter}
@Card.Text{You have clicked @ur[count] times at the button.}]}
@br[]
@Button[onClick: @ur[on-button-click]]{Click me}]])
;;;
;;; UTILS
;;;
(define ($0 selector) (ref ($ selector) 0))
;;;
;;; Replace elements with React components
;;;
(def App App1) ; Try the 3 versions one at a time.
;; (def App App2)
;; (def App App3)
(let ([elm ($0 "#the-body-element")])
(unless (= elm undefined)
(def props (object))
(ReactDOM.render (React.createElement App props) elm)))
)))
;;;
;;; Self-contained Example
;;;
(define (script url) @~a{<script src=@url ></script>})
(define (link-css url) @~a{<link href=@url rel="stylesheet">})
(define react-bootstrap-js
"https://unpkg.com/react-bootstrap@next/dist/react-bootstrap.min.js")
(define (generate-html body urlang-js-file)
@~a{
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap -->
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
crossorigin="anonymous"/>
</head>
<body>
@body
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
crossorigin="anonymous"></script>
<script src="https://unpkg.com/react/umd/react.development.js" crossorigin="anonymous"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js" crossorigin="anonymous"></script>
<script src="https://unpkg.com/react-bootstrap@"@"next/dist/react-bootstrap.js"
crossorigin="anonymous"></script>
<script>@file->string[urlang-js-file]</script>
</body>
</html>})
(require net/sendurl)
(send-url/contents
(generate-html
@~a{<div id="the-body-element"></div>} ; This will be replaced with (App ...)
react-example.js))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment