Skip to content

Commit

Permalink
adding extism enhance elixir example
Browse files Browse the repository at this point in the history
  • Loading branch information
Benanna2019 committed Jun 10, 2024
1 parent 2f2f04d commit a37cad9
Show file tree
Hide file tree
Showing 51 changed files with 2,063 additions and 0 deletions.
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

If you would like to watch a full video on this, you can do so here:

[![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

0 comments on commit a37cad9

Please sign in to comment.