Created
September 23, 2020 17:30
-
-
Save soegaard/0622c798426f2aef168b6d4dc6568ca5 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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