Skip to content

Commit

Permalink
Generic JSX transform (#9)
Browse files Browse the repository at this point in the history
* wire up the generic JSX transform

* move more to generic jsx transform

* docs

* restructure more

* latest versions of Core and Bun

* up and recompile

* fix versions

* note about versions
  • Loading branch information
zth authored Feb 14, 2024
1 parent d72236d commit 83c67c4
Show file tree
Hide file tree
Showing 58 changed files with 5,160 additions and 2,302 deletions.
99 changes: 53 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,19 @@ First, make sure you have [`Bun`](https://bun.sh) installed and setup. Then, ins
npm i rescript-x vite @rescript/core rescript-bun
```

Note that ResX requires these versions:

- `rescript@>=11.1.0-rc.2`
- `@rescript/core@>=1.0.0`
- `rescript-bun@>=0.4.1`

Configure our `rescript.json`:

```json
{
"jsx": {
"module": "Hjsx"
},
"bs-dependencies": ["@rescript/core", "rescript-x", "rescript-bun"],
"bsc-flags": [
"-open RescriptCore",
Expand Down Expand Up @@ -153,12 +162,12 @@ let server = Bun.serve({
<div>
{switch appRoutes {
| list{} =>
<div> {H.string("Start page!")} </div>
<div> {Hjsx.string("Start page!")} </div>
| list{"moved"} =>
requestController->ResX.RequestController.redirect("/start", ~status=302)
| _ =>
requestController->ResX.RequestController.setStatus(404)
<div>{H.string("404")}</div>
<div>{Hjsx.string("404")}</div>
}}
</div>
</Html>
Expand Down Expand Up @@ -195,22 +204,17 @@ You route by just pattern matching on `path`:
switch path {
| list{} =>
// Path: /
<div> {H.string("Start page!")} </div>
<div> {Hjsx.string("Start page!")} </div>
| list{"moved"} =>
// Path: /moved
requestController->ResX.RequestController.redirect("/start", ~status=302)
| _ =>
// Any other path
requestController->ResX.RequestController.setStatus(404)
<div>{H.string("404")}</div>
<div>{Hjsx.string("404")}</div>
}
```

### State of ResX (read: caveats)

- You'll see some "react" in the code. This is because we're currently piggy backing on the React JSX integration. This will change as a generic JSX transform is shipped to ReScript in the future.
- Autocomplete for HTMX and ResX HTML element prop names currently does not work. This will also change as a generic transform is shipped.

## Static assets

ResX comes with full static asset (fonts, images, etc) handling via Vite, that you can use if you want. In order to actually serve the static assets, make sure you use `ResX.BunUtils.serveStaticFile` before trying to handle your request in another way:
Expand Down Expand Up @@ -316,25 +320,25 @@ let onForm = Handler.handler->ResX.Handlers.hxPost("/user-single", ~handler=asyn
let formData = await request->Request.formData
try {
let name = formData->ResX.FormDataHelpers.expectString("name")
<div>{H.string(`Hi ${name}!`)}</div>
<div>{Hjsx.string(`Hi ${name}!`)}</div>
} catch {
| Exn.Error(err) =>
Console.error(err)
<div> {H.string("Failed...")} </div>
<div> {Hjsx.string("Failed...")} </div>
}
})
@react.component
@jsx.component
let make = () => {
<form
hxPost={onForm}
hxSwap={ResX.Htmx.Swap.make(InnerHTML)}
hxTarget={ResX.Htmx.Target.make(CssSelector("#user-single"))}>
<input type_="text" name="name" />
<div id="user-single">
{H.string("Hello...")}
{Hjsx.string("Hello...")}
</div>
<button>{H.string("Submit")}</button>
<button>{Hjsx.string("Submit")}</button>
</form>
}
```
Expand All @@ -355,25 +359,25 @@ Handler.handler->ResX.Handlers.implementHxPostIdentifier(onForm, ~handler=async
let formData = await request->Request.formData
try {
let name = formData->ResX.FormDataHelpers.expectString("name")
<div>{H.string(`Hi ${name}!`)}</div>
<div>{Hjsx.string(`Hi ${name}!`)}</div>
} catch {
| Exn.Error(err) =>
Console.error(err)
<div> {H.string("Failed...")} </div>
<div> {Hjsx.string("Failed...")} </div>
}
})
@react.component
@jsx.component
let make = () => {
<form
hxPost={onForm}
hxSwap={ResX.Htmx.Swap.make(InnerHTML)}
hxTarget={ResX.Htmx.Target.make(CssSelector("#user-single"))}>
<input type_="text" name="name" />
<div id="user-single">
{H.string("Hello...")}
{Hjsx.string("Hello...")}
</div>
<button>{H.string("Submit")}</button>
<button>{Hjsx.string("Submit")}</button>
</form>
}
```
Expand All @@ -387,17 +391,17 @@ Notice how producing the `hxPost` identitifer is now separate from implementing
All `hx`-attributes have type safe maker-style APIs. Let's look at the example above again:

```rescript
@react.component
@jsx.component
let make = () => {
<form
hxPost={onForm}
hxSwap={ResX.Htmx.Swap.make(InnerHTML)}
hxTarget={ResX.Htmx.Target.make(CssSelector("#user-single"))}>
<input type_="text" name="name" />
<div id="user-single">
{H.string("Hello...")}
{Hjsx.string("Hello...")}
</div>
<button>{H.string("Submit")}</button>
<button>{Hjsx.string("Submit")}</button>
</form>
}
```
Expand All @@ -416,10 +420,10 @@ let onSubmit = Handler.handler->ResX.Handlers.formAction("/some-url", ~handler=a
Response.makeRedirect("/some-other-page")
})
@react.component
@jsx.component
let make = () => {
<form action={onSubmit}>
<button>{H.string("Submit and get redirected!")}</button>
<button>{Hjsx.string("Submit and get redirected!")}</button>
</form>
}
```
Expand Down Expand Up @@ -448,7 +452,7 @@ Sometimes all you need to do is add, remove or toggle a CSS class in response to
resXOnClick={ResX.Client.Actions.make([
ToggleClass({className: "text-xl", target: This}),
])}>
{H.string("Submit form")}
{Hjsx.string("Submit form")}
</button>
```

Expand Down Expand Up @@ -477,39 +481,42 @@ This will turn the validity message for when the value is missing (since it's ma

## Building UI with ResX

If you're familiar with React and the component model, building UI with ResX is very straight forward. It's essentially like using React as a templating engine, with a sprinkle of React Server Components flavor.
If you're familiar with React, JSX and the component model, building UI with ResX is very straight forward. It's essentially like using React as a templating engine, with a sprinkle of React Server Components flavor.

In ResX, you'll interface with 2 modules mainly when working with JSX:

The bulk of your code is going to be (reusable) components. You define one just like you do in React, with the difference that `React.string`, `React.int` etc are called `H.string` and `H.int` instead:
1. `Hjsx` - this holds functions like `string`, `int` etc for converting primitives to JSX, and a bunch of things that are needed for the JSX transform.
2. `H` - this holds the `Context` module, as well as functions for turning JSX elements into strings.

The bulk of your code is going to be (reusable) components. You define one just like you do in React, with the difference that `React.string`, `React.int` etc are called `Hjsx.string` and `Hjsx.int`, and `@react.component` is called `@jsx.component` instead:

```rescript
// Greet.res
@react.component
@jsx.component
let make = (~name) => {
<div>{H.string("Hello " ++ name)}</div>
<div>{Hjsx.string("Hello " ++ name)}</div>
}
// SomeFile.res
@react.component
@jsx.component
let make = (~userName) => {
<div>
<Greet name=userName />
</div>
}
```

> Note: `@react.component` will likely be called `@jsx.component` or similar in the future, when the generic JSX transform lands in ReScript.
### Async components

Components can be defined using `async`/`await`. This enables you to do data fetching directly in them:

```rescript
// User.res
@react.component
@jsx.component
let make = async (~id) => {
let user = await getUser(id)
<div>{H.string("Hello " ++ user.name)}</div>
<div>{Hjsx.string("Hello " ++ user.name)}</div>
}
```

Expand All @@ -529,7 +536,7 @@ module Provider = {
let make = H.Context.provider(context)
}
@react.component
@jsx.component
let make = (~children, ~currentUserId: option<string>) => {
<Provider value={currentUserId}> {children} </Provider>
}
Expand All @@ -542,11 +549,11 @@ let currentUserId = request->UserUtils.getCurrentUserId
// LoggedInUser.res
// This is rendered somewhere far down in the tree
@react.component
@jsx.component
let make = () => {
switch CurrentUserId.use() {
| None => <div>{H.string("Not logged in")}</div>
| Some(currentUserId) => <div>{H.string("Logged in as: " ++ currentUserId)}</div>
| None => <div>{Hjsx.string("Not logged in")}</div>
| Some(currentUserId) => <div>{Hjsx.string("Logged in as: " ++ currentUserId)}</div>
}
}
```
Expand All @@ -558,7 +565,7 @@ Just like in React, you can protect parts of your UI from errors during render u
```rescript
<ResX.ErrorBoundary renderError={err => {
Console.error(err)
<div>{H.string("Oops, this blew up!")}</div>
<div>{Hjsx.string("Oops, this blew up!")}</div>
}}>
<div>
<ComponentThatWillBlowUp />
Expand Down Expand Up @@ -682,12 +689,12 @@ You can set the response status anywhere when rendering:

```rescript
// FourOhFour.res
@react.component
@jsx.component
let make = () => {
let context = ResX.Handlers.useContext(HtmxHandler.handler)
context.requestController->ResX.RequestController.setStatus(404)
<div> {H.string("404")} </div>
<div> {Hjsx.string("404")} </div>
}
```

Expand All @@ -706,7 +713,7 @@ By default, any returned content from your handlers is prefixed with `<!DOCTYPE

```rescript
// SiteMap.res
@react.component
@jsx.component
let make = () => {
let context = ResX.Handlers.useContext(HtmxHandler.handler)
Expand All @@ -722,10 +729,10 @@ let make = () => {
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc> {H.string("https://www.example.com/")} </loc>
<lastmod> {H.string("2023-10-15")} </lastmod>
<changefreq> {H.string("weekly")} </changefreq>
<priority> {H.string("1.0")} </priority>
<loc> {Hjsx.string("https://www.example.com/")} </loc>
<lastmod> {Hjsx.string("2023-10-15")} </lastmod>
<changefreq> {Hjsx.string("weekly")} </changefreq>
<priority> {Hjsx.string("1.0")} </priority>
</url>
</urlset>
}
Expand Down
Loading

0 comments on commit 83c67c4

Please sign in to comment.