Skip to content

e3phoenix: Adding enhance elixir extism example #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 171 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,172 @@
# enhance-ssr-elixir-phoenix

Here is a video if you would like to watch it:

[![enhance-elixir](https://github.com/Benanna2019/enhance-ssr-elixir-phoenix/assets/65513685/86412b68-0b9e-4cec-8d27-94dddc4c4476)](https://www.youtube.com/watch?v=LVlDhNxsSTQ)

Setup Steps

1. Install Rust (if not already installed) - run this in a terminal `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh`
2. Install elixir - probably just use homebrew
3. Make sure you have postgres setup and installed - (I use postgresapp.com)

Create a New Phoenix Project (or clone this project)

- run `mix phx.new name_of_app --live`
- You have to give the app a name or it will fail. The `--live` option is to say this is a LiveView project

Add extism as a dependency

- in the mix.exs file add `{:extism, "1.0.0"}`
- the run `mix deps.get`

Adding enhance-ssr/wasm

- Create wasm directory
- Download the enhance wasm file into local directory - `curl -L [https://github.com/enhance-dev/enhance-ssr-wasm/releases/download/v0.0.3/enhance-ssr.wasm.gz](https://github.com/enhance-dev/enhance-ssr-wasm/releases/download/v0.0.3/enhance-ssr.wasm.gz) | gunzip > wasm/enhance-ssr.wasm`

A little extra setup (for a basic phoenix project)

- find your router.ex file under `lib/[name_of_project]_web`
- add a new route something similar to `live "/enhance", EnhanceLive` underneath the `get "/"`
- now create a `live` folder in `lib/[name_of_project]_web`
- now create an `enhance_live.ex` file
- This will be the module that is responsible for our view when navigating to `localhost:4000/enhance`

Creating an Extism Plugin

- Look at extism elixir docs → Show that we need to create a plugin in a very specific way
[https://extism.org/docs/quickstart/host-quickstart/](https://extism.org/docs/quickstart/host-quickstart/)
- Create an Elixir/Phoenix module in `lib/[name_of_project]_web` called `SsrWebComponentsOnTheBeam.ConvertComponents` that ‘creates_plugin’

```elixir
defmodule SsrWebComponentsOnTheBeam.ConvertComponents do
@wasm_plugin_path Path.expand("../../../wasm/enhance-ssr.wasm", __DIR__)

def create_plugin do
# Define the path to your local WASM file

IO.inspect "Creating plugin with path: #{@wasm_plugin_path}"

# Create the manifest with the local file path
manifest = %{wasm: [%{path: @wasm_plugin_path}]}

# Create the plugin with Extism.Plugin.new
case Extism.Plugin.new(manifest, true) do
{:ok, plugin} ->
{:ok, plugin}

{:error, reason} ->
{:error, reason}
end
end
end
```

- Pull up enhance documentation for what enhance expects as a function signature
[GitHub - enhance-dev/enhance-ssr-wasm: Enhance SSR compiled for WASM](https://github.com/enhance-dev/enhance-ssr-wasm?tab=readme-ov-file#usage)
- Create a ‘call_enhance_plugin’ function

```elixir
defmodule SsrWebComponentsOnTheBeam.ConvertComponents do
@wasm_plugin_path Path.expand("../../../wasm/enhance-ssr.wasm", __DIR__)

def create_plugin do
# Define the path to your local WASM file

IO.inspect "Creating plugin with path: #{@wasm_plugin_path}"

# Create the manifest with the local file path
manifest = %{wasm: [%{path: @wasm_plugin_path}]}

# Create the plugin with Extism.Plugin.new
case Extism.Plugin.new(manifest, true) do
{:ok, plugin} ->
{:ok, plugin}

{:error, reason} ->
{:error, reason}
end
end

def call_enhance_plugin(plugin, data) do
Extism.Plugin.call(plugin, "ssr", Jason.encode!(data))
end
end
```

- decode the output which should just be a variable called enhance
- get the document off of the enhance output and return in in a the raw function in a `<%= =>` expression in a `~H` template

```elixir
defmodule SsrWebComponentsOnTheBeam.EnhanceLive do
use SsrWebComponentsOnTheBeam, :live_view
use Phoenix.Component

alias SsrWebComponentsOnTheBeam.ConvertComponents

def mount(_params, _session, socket) do
socket =
socket
|> assign(:color, "text-red-500")

{:ok, socket}
end

def render(assigns) do
~H"""
<.enhance_header id='my-header' color={@color} />

<button phx-click="change-color">Change color to red</button>
"""
end

def enhance_header(assigns) do

IO.puts "assigns: #{inspect(assigns)}"

data = %{
markup: "<my-header id='my-header' color=#{assigns.color}>Hello World</my-header>",
elements: %{
"my-header":
"function MyHeader({ html, state }) {
const { attrs, store } = state
const attrs_color = attrs['color']
const id = attrs['id']
const store_works = store['readFromStore']
return html`<h1 class='${attrs_color}'><slot></slot></h1><p>store works: ${store_works} </p><p>attrs id: ${id} </p><p>attrs color: ${attrs_color} </p>`
}",
},
initialState: %{ readFromStore: "true" },
}

{:ok, plugin} = ConvertComponents.create_plugin()

{:ok, output} = ConvertComponents.call_enhance_plugin(plugin, data)

html = Jason.decode!(output)

~H"""
<div>
<%= raw(html["document"]) %>
</div>
"""

end

def handle_event("change-color", _, socket) do
{:noreply, assign(socket, :color, "text-blue-500")}
end

end
```

Checking the output

- Lastly we want to make sure that we are in fact getting our web components server rendered. So if you navigate to `localhost:4000/enhance` and inspect the page, you should see something like this.

<img width="654" alt="Screen Shot 2024-06-09 at 12 28 08 PM" src="https://github.com/Benanna2019/enhance-ssr-elixir-phoenix/assets/65513685/22a0da79-15c5-4947-a238-3735ec63722f">

If you look at the `<my-header></my-header>` element, you should see this attribute, `enhanced="✨"` signifying that you are using the enhance-ssr package to server render your custom elements.

Huzza! Much love to Extism, Enhance, Elixir, and Phoenix Liveview. So many cool things working together.
5 changes: 5 additions & 0 deletions ssr_web_components_on_the_beam/.formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[
import_deps: [:ecto, :phoenix],
inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"],
subdirectories: ["priv/*/migrations"]
]
34 changes: 34 additions & 0 deletions ssr_web_components_on_the_beam/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Ignore package tarball (built via "mix hex.build").
ssr_web_components_on_the_beam-*.tar

# Ignore assets that are produced by build tools.
/priv/static/assets/

# Ignore digested assets cache.
/priv/static/cache_manifest.json

# In case you use Node.js/npm, you want to ignore these.
npm-debug.log
/assets/node_modules/

123 changes: 123 additions & 0 deletions ssr_web_components_on_the_beam/assets/css/app.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";

/* This file is for your main application CSS */

/* Alerts and form errors used by phx.new */
.alert {
padding: 15px;
margin-bottom: 20px;
border: 1px solid transparent;
border-radius: 4px;
}
.alert-info {
color: #31708f;
background-color: #d9edf7;
border-color: #bce8f1;
}
.alert-warning {
color: #8a6d3b;
background-color: #fcf8e3;
border-color: #faebcc;
}
.alert-danger {
color: #a94442;
background-color: #f2dede;
border-color: #ebccd1;
}
.alert p {
margin-bottom: 0;
}
.alert:empty {
display: none;
}
.invalid-feedback {
color: #a94442;
display: block;
margin: -1rem 0 2rem;
}

/* LiveView specific classes for your customization */
.phx-no-feedback.invalid-feedback,
.phx-no-feedback .invalid-feedback {
display: none;
}

.phx-click-loading {
opacity: 0.5;
transition: opacity 1s ease-out;
}

.phx-loading{
cursor: wait;
}

.phx-modal {
opacity: 1!important;
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0,0,0,0.4);
}

.phx-modal-content {
background-color: #fefefe;
margin: 15vh auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
}

.phx-modal-close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
}

.phx-modal-close:hover,
.phx-modal-close:focus {
color: black;
text-decoration: none;
cursor: pointer;
}

.fade-in-scale {
animation: 0.2s ease-in 0s normal forwards 1 fade-in-scale-keys;
}

.fade-out-scale {
animation: 0.2s ease-out 0s normal forwards 1 fade-out-scale-keys;
}

.fade-in {
animation: 0.2s ease-out 0s normal forwards 1 fade-in-keys;
}
.fade-out {
animation: 0.2s ease-out 0s normal forwards 1 fade-out-keys;
}

@keyframes fade-in-scale-keys{
0% { scale: 0.95; opacity: 0; }
100% { scale: 1.0; opacity: 1; }
}

@keyframes fade-out-scale-keys{
0% { scale: 1.0; opacity: 1; }
100% { scale: 0.95; opacity: 0; }
}

@keyframes fade-in-keys{
0% { opacity: 0; }
100% { opacity: 1; }
}

@keyframes fade-out-keys{
0% { opacity: 1; }
100% { opacity: 0; }
}
Loading
Loading