diff --git a/src/app.rkt b/src/app.rkt index bbcf95c..ef7943d 100644 --- a/src/app.rkt +++ b/src/app.rkt @@ -1,168 +1,108 @@ -#lang racketscript/base +#lang at-exp racketscript/base -(require "./counter.rkt" +(require (for-syntax racket/base + racket/file + racket/syntax + syntax/parse) + "./counter.rkt" "./todo.rkt" rackt) -(define (header props . ..) - ( "header" + (<> "div" #:props ([ className "header-content" ]) + (<> "img" #:props ([ src "https://raw.githubusercontent.com/rackt-org/rackt-org.github.io/master/logo.png" ] + [ className "logo" ])) + (<> "h1" (<> "a" #:props ([ href "https://github.com/rackt-org/rackt" ]) "Rackt")) + (<> "p" "A React wrapper written in " + (<> "a" #:props ([ href "https://github.com/vishesh/racketscript" ]) "RacketScript"))))) + +(define-component intro + (<> "div" + @<>["p"]{Rackt is a React wrapper that allows you to write functional components with React hooks, contexts, and so on. + As a first step, you can use almost exactly the same React API that you're used to because it seemlessly interoperates with JS under the hood. + Here is an example of a simple Rackt component:} + (<> "pre" + (<> "code" #:props ($/obj [ className "language-racket" ]) "(define (simple-component props . ..) - ( \"div\" #:props ([ className \"some-class\" ]) \"some text\"))")) - ( "p" (<> "code" "<>") " here is a simple alias for the " (<> "code" "React.createElement") " function + that has optional an " (<> "code" "#:props") " parameter so you can skip it if you want:") + (<> "pre" + (<> "code" #:props ([ className "language-racket" ]) "(define (simple-component props . ..) - (string ($ e 'target 'value)) text)) - - (define (submit-todo e) - (($ e 'preventDefault)) - (dispatch ($/obj [ type \"add\" ] - [ todo ($/obj [ id (#js*.Date.now) ] - [ text text ])])) - - (set-text \"\")) - - ( \"div\" \"some text\"))")) + @<>["p"]{Because it's written with RacketScript, however, Rackt takes advantage of Racket's powerful macros in order to simplify and hide a lot of boilerplate that you would normally have to write in plain JS:} + + (<> "pre" + (<> "code" #:props ($/obj [ className "language-racket" ]) + "(define-component simple-component + (<> \"div\" #:props ([ className \"some-class\" ]) ($props 'value))")) + + (<> "p" "Here " (<> "code" "define-component") " defines a React component, but automatically propagates the \"props\" for you in the implicit " (<> "code" "$props") " variable.") + + (<> "p" "In the examples below you can see more complex components and apps (btw this site is written in Rackt as well), and how Rackt and RacketScript help to manage and reduce this complexity."))) + +(define-component counter-example + (<> "div" #:props ([ className "example" ]) + (<> "div" + (<> "h3" "Counter") + (<> counter)) + (<> "div" + (<> "h3" "Source code") + (<> "pre" (<> "code" #:props ([ className "language-racket"]) counter-source-code))))) + +(define-component todo-example + (<> "div" #:props ([ className "example" ]) + (<> "div" + (<> "h3" "Todo app") + (<> todo-app)) + (<> "div" + (<> "h3" "Source code") + (<> "pre" (<> "code" #:props ([ className "language-racket"]) todo-source-code))))) + + +(define-component todo-orig-example + (<> "div" #:props ([ className "example" ]) + (<> "div" + (<> "p" "For reference, here is the same todo app written with a more direct React API") + (<> "pre" (<> "code" #:props ([ className "language-racket"]) todo-orig-source-code))))) + +(define-component app + (<> "div" #:props ([ className "container" ]) + (<> header) + (<> intro) + (<> "h2" "Examples") + (<> counter-example) + (<> todo-example) + (<> todo-orig-example))) + +(require (for-syntax mzlib/etc)) +(define-for-syntax this-dir (this-expression-source-directory)) + +;; get src code strs, at compile time, from actual file +(define-syntax define-src-code-str-def + (syntax-parser + [(_ name:id) + #:with def-name (format-id #'name "~a-source-code" #'name) + #:with str-name (format-id #'name "~a-src-code-str" #'name) + #`(define def-name #,(syntax-local-value #'str-name))])) + +;; TODO: +;; need get these first, rather than in the macro, +;; bc racketscript seems to expand twice? +(define-syntax counter-src-code-str + (file->string (build-path this-dir "counter.rkt"))) +(define-syntax todo-src-code-str + (file->string (build-path this-dir "todo.rkt"))) +(define-syntax todo-orig-src-code-str + (file->string (build-path this-dir "todo-orig.rkt"))) + +(define-src-code-str-def counter) +(define-src-code-str-def todo) +(define-src-code-str-def todo-orig) + +(render (<> app) "root") diff --git a/src/counter-orig.rkt b/src/counter-orig.rkt new file mode 100644 index 0000000..7c45190 --- /dev/null +++ b/src/counter-orig.rkt @@ -0,0 +1,24 @@ +#lang racketscript/base + +(require racketscript/interop + rackt) + +(define (counter props ..) + (define-values (counter set-counter) (use-state 0)) + + ( "div" + (<> "button" #:props ([ className "button" ] + [ type "button" ] + [ onClick (lambda (_) (set-COUNT! (sub1 COUNT))) ]) + "- 1") - ( "span" #:props ([ className "counter" ]) COUNT) - ( "button" #:props ([ className "button" ] + [ type "button" ] + [ onClick (lambda (_) (set-COUNT! (add1 COUNT))) ]) + "+ 1"))) (provide counter) diff --git a/src/todo-orig.rkt b/src/todo-orig.rkt new file mode 100644 index 0000000..0314f40 --- /dev/null +++ b/src/todo-orig.rkt @@ -0,0 +1,89 @@ +#lang racketscript/base + +(require racketscript/interop + rackt) + +(define StateContext (create-context)) + +(define default-state + ($/obj [ todos (list + ($/obj [ id 0 ] [ text "Replace JavaScript with RacketScript"]) + ($/obj [ id 1 ] [ text "Install Rackt" ]) + ($/obj [ id 2 ] [ text "Enjoy!" ]))])) + +(define (add-todo state action) + (append ($ state 'todos) (list ($ action 'todo)))) + +(define (done-todo state action) + (filter (lambda (el) (not (eq? ($ el 'id) ($ action 'id)))) ($ state 'todos))) + +(define (reducer state action) + (cond + [(eq? ($ action 'type) "add") + ($/obj [ todos (add-todo state action)])] + [(eq? ($ action 'type) "done") + ($/obj [ todos (done-todo state action)])] + [else state])) + +(define (todo-input props . ..) + (define ctx (use-context StateContext)) + (define dispatch ($ ctx 'dispatch)) + (define store ($ ctx 'store)) + (define-values (text set-text) (use-state "")) + + (define (update-text e) + (set-text (js-string->string ($ e 'target 'value)) text)) + + (define (submit-todo e) + (($ e 'preventDefault)) + (dispatch ($/obj [ type "add" ] + [ todo ($/obj [ id (#js*.Date.now) ] + [ text text ])])) + + (set-text "")) + + ( "div" + (<> todo-input) + (<> todo-list)))) + +(define-component todo-input + ;; The following implicit vars are available in the body of in-reducer-context, + ;; - $ctx-state: the Reducer's state + ;; - $ctx-dispatch: the Reducer's dispatch function + (in-reducer-context TodoListContext + + (define-state TEXT "") (define (update-text e) - (set-text (js-string->string ($ e 'target 'value)) text)) - - (define (submit-todo e) - (($ e 'preventDefault)) - (dispatch ($/obj [ type "add" ] - [ todo ($/obj [ id (#js*.Date.now) ] - [ text text ])])) - - (set-text "")) - - (string ($ e 'target 'value)) TEXT)) + + (<> "form" + #:props ([onSubmit + (lambda (e) + (($ e 'preventDefault)) + ($ctx-dispatch add [todo (mk-todo-item (#js*.Date.now) TEXT)]) + (set-TEXT! ""))]) + (<> "input" + #:props ([ className "todo-input" ] + [ placeholder "What needs to be done?" ] + [ value TEXT ] + [ onChange update-text]))))) + +;; A todo-list component consists of a list of todo-item components +(define-component todo-list + (define (mk-todo-item-component todo) (<> todo-item #:props ([todo todo]))) + (in-reducer-context TodoListContext + (<> "ul" (map mk-todo-item-component ($ctx-state 'todos))))) + +(define-component todo-item + (in-reducer-context TodoListContext + (<> "li" #:props ([ className "todo-item"]) + ($props 'todo 'text) + (<> "button" + #:props ([ type "button" ] + [ className "button button-clear todo-done-button"] + [ onClick (lambda (_) ($ctx-dispatch done [id ($props 'todo 'id)]))]) + "✔")))) (provide todo-app)