https://github.com/feedbackone/elmstronaut
Render Elm modules as Astro components
https://github.com/feedbackone/elmstronaut
astro astro-integration elm
Last synced: 16 days ago
JSON representation
Render Elm modules as Astro components
- Host: GitHub
- URL: https://github.com/feedbackone/elmstronaut
- Owner: feedbackone
- License: mit
- Created: 2025-02-25T20:43:07.000Z (2 months ago)
- Default Branch: main
- Last Pushed: 2025-02-28T09:39:13.000Z (2 months ago)
- Last Synced: 2025-03-22T09:12:51.804Z (about 1 month ago)
- Topics: astro, astro-integration, elm
- Language: TypeScript
- Homepage: https://www.npmjs.com/package/elmstronaut
- Size: 192 KB
- Stars: 24
- Watchers: 1
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
Awesome Lists containing this project
- awesome-ccamel - feedbackone/elmstronaut - Render Elm modules as Astro components (TypeScript)
README
# Elmstronaut 🌳👨🚀
> That's one small step for (a) man, one giant leap for Elm-kind.
An Astro integration that enables rendering of Elm modules as Astro components.
## Table of contents
- [Installation](#installation)
- [Setup](#setup)
- [Basic usage](#basic-usage)
- [Fallback slot](#fallback-slot)
- [Passing flags](#passing-flags)
- [Using ports](#using-ports)
- [Tailwind support](#tailwind-support)
- [Examples](#examples)
- [Limitations](#limitations)
- [Future plans](#future-plans)
- [Contributing](#contributing)## Installation
```sh
pnpm add elm elmstronaut
```## Setup
This guide assumes you already have an Astro project set up. If not, please run `pnpm create astro@latest` first and come back when you're ready.
- Create a folder called `elm` under the `src` directory. Your Elm files will live here.
- Make sure there is an _elm.json_ file in the root directory. Run `pnpm elm init` if you haven't initialized your Elm project yet.
- Modify `"source-directories"` from `src` to `src/elm` in the _elm.json_```diff
"source-directories": [
- "src"
+ "src/elm"
],
```
- Add `elmstronaut` to Astro integrations in the _astro.config.mts_```diff
+ import elmstronaut from "elmstronaut";export default defineConfig({
+ integrations: [elmstronaut()],
});
```## Basic usage
Let's start with a canonical "Hello, world" example.
_src/elm/Hello.elm_
```elm
module Hello exposing (main)import Html exposing (Html, text)
main : Html msg
main =
text "Hello, Astro 👋"
```_src/pages/index.astro_
```jsx
---
import Hello from "../elm/Hello.elm";
import Layout from "../layouts/Layout.astro";
---
```
> [!IMPORTANT]
> Notice the `client:load` directive. This is essential as we don't support SSR yet. Hopefully, some day in the near future 🤞.Congratulations!
We can now use Elm components in Astro! 🎉## Fallback slot
You can also pass an optional "fallback" slot to display while the component is loading.
```jsx
---
import Hello from "../elm/Hello.elm";
import Layout from "../layouts/Layout.astro";
---
Loading...
```
This will improve the user experience, and decrease the [CLS](https://web.dev/articles/cls) score of your page.
## Passing flags
Component props are automatically passed as flags to your Elm app. Although you can access them directly (don't do this – there is a reason you're using Elm after all), the proper way is to decode them.
Let's take a look at another widely known example – the Counter!
_src/pages/counter.astro_
```jsx
---
import Counter from "../elm/Counter.elm";
import Layout from "../layouts/Layout.astro";
---
```
_src/elm/Counter.elm_
```elm
module Counter exposing (main)import Browser
import Html exposing (Html, button, div, p, text)
import Html.Events exposing (onClick)
import Json.Decode-- MAIN
main : Program Json.Decode.Value Model Msg
main =
Browser.element
{ init = init
, update = update
, subscriptions = \_ -> Sub.none
, view = view
}-- FLAGS
type alias Flags =
{ initial : Int }flagsDecoder : Json.Decode.Decoder Flags
flagsDecoder =
Json.Decode.map Flags
(Json.Decode.field "initial" Json.Decode.int)-- MODEL
type alias Model =
{ count : Int }init : Json.Decode.Value -> ( Model, Cmd Msg )
init flags =
let
initialCount =
Json.Decode.decodeValue flagsDecoder flags
|> Result.map .initial
|> Result.withDefault 0
in
( { count = initialCount }, Cmd.none )-- UPDATE
type Msg
= Increment
| Decrementupdate : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Increment ->
( { model | count = model.count + 1 }, Cmd.none )Decrement ->
( { model | count = model.count - 1 }, Cmd.none )-- VIEW
view : Model -> Html Msg
view model =
div []
[ button [ onClick Increment ] [ text "+" ]
, p [] [ text (String.fromInt model.count) ]
, button [ onClick Decrement ] [ text "-" ]
]
```Let's walk trough the important bits:
- First, we pass `Json.Decode.Value` as the type of the second argument of the `main` function.
- Then, we define the `Flags` type and its decoder.
- Lastly, we pass the decoder we defined above and the `flags` argument of the `init` function to the `Json.Decode.decodeValue` function. If the decoding succeeds, the `initialCount` would get the value of the `initial` prop. Otherwise, it will be set to `0`. No runtime errors. Beauty!
> [!TIP]
> [NoRedInk/elm-json-decode-pipeline](https://package.elm-lang.org/packages/NoRedInk/elm-json-decode-pipeline/) package immensely simplifies the process of writing decoders.## Using ports
To use ports we need to define `window.onElmInit`. It receives a callback, which will be called each time an Elm app is initialized. For each initialization it's corresponding Elm module name and the app will be passed as arguments.
_src/elm/interop.ts_ (or other)
```ts
window.onElmInit = (elmModuleName: string, app: ElmApp) => {
if (elmModuleName === "Hello") {
// Subscribe to messages from Elm
app.ports?.foo.subscribe?.((message) => console.log(message));// Send messages to Elm
app.ports?.bar.send?.("baz");
}
};
```The `elmModuleName` is the module name provided in the Elm file.
For example, if `Hello.elm` would have been located at `src/elm/Greeting/Hello.elm` instead of `src/elm/Hello.elm` as mentioned in the examples above, the `elmModuleName` would be `Greeting.Hello`.
## Tailwind support
If you're using [Tailwind](https://tailwindcss.com/) in your Elm files, make sure to add the following spinnet to your CSS:
```diff
@import "tailwindcss";+ @source "../../src/elm";
```This ensures that the classes used in the Elm files would be included in the final bundle.
## Examples
The [examples](https://github.com/feedbackone/elmstronaut/tree/main/examples) folder could be a useful place to start. Altough it currently only contains a few basic examples, we're planning to add more in the near future.## Limitations
- Can't render nested components (POC is ready)
- No SSR support (yet)
- Only `Browser.element` is supported. **This is by design.** The routing part will always be handled by Astro.## Future plans
- [ ] Add support for rendering named slots.
- [ ] "Go to definition" should open the Elm file instead of the `elmstronaut.d.ts`.
- [ ] Add SSR support.
- [ ] Figure out a way to compile multiple Elm modules into one bundle.
- [ ] Remove the constraint of having the `elm` folder.
- [ ] Add an `optimize` option to the config to force production builds when needed.
- [ ] Add an `elmJsonPath` option to be able to specify the path to the _elm.json_ file.
- [ ] Generate an Elm custom type with all possible routes based on the `pages` folder, so that we can use `href` safely (similar to Elm Land).
- [ ] Generate a type union of all Elm module names. We can then use that type instead of `string` for `elmModuleName`.
- [ ] Parse Elm files and generate proper types for ports.## Contributing
Please check out our contributing guidelines [here](/CONTRIBUTING.md).