I fiddled around a bit more yesterday with my Phoenix.Component → Stimulus → React idea. (I don’t know if this is the best place to share it, but in case anyone is going down the same path, here it goes):
components/form_component.ex
:
this just calls a react/1
function component with controller
(name of the Stimulus controller) and props
(a map of props that will be provided to the React component):
def multi_select(assigns) do
~H"""
<.react controller="multi-select" props={extra_attributes(assigns)}>
<%= Phoenix.HTML.Form.multiple_select(@form, @field, @options, class: "multi-select__select", data: [multi_select_target: "select"]) %>
<div data-multi-select-target="component"></div>
</.react>
"""
end
components/react_component.ex
:
pretty straightforward also, outputs something like <div data-controller=“multi-select” data-multi-select-props-value={…} />`
defmodule MyAppWeb.ReactComponent do
use Phoenix.Component
import Phoenix.Naming, only: [camelize: 2]
@react_props %{class: :class_name}
def react(assigns) do
assigns = assigns |> assign(:data, controller: assigns.controller, "#{props_key(assigns)}": props_value(assigns))
~H"""
<div data={@data} class="react">
<%= if @inner_block do %><%= render_slot(@inner_block) %><% end %>
</div>
"""
end
defp props_key(assigns) do
"#{assigns.controller}-props-value"
end
defp props_value(assigns) do
assigns.props
|> Enum.map(fn {key, value} -> {Map.get(@react_props, key, key), value} end)
|> Enum.map(fn {key, value} -> {camelize(to_string(key), :lower), value} end)
|> Map.new()
|> Jason.encode!()
end
end
stimulus_controllers/multi_select_controller.jsx
and then this connects the 3rd party React component to the DOM:
import { MultiSelect } from '@mantine/core';
export default class extends Controller {
static values = { props: Object };
static targets = ['select', 'component'];
connect() {
ReactDOM.render(
<MultiSelect ... data={this.options} onChange={this.handleChange} />,
this.componentTarget
);
}
disconnect() {
ReactDOM.unmountComponentAtNode(this.componentTarget);
}
handleChange = (values) => { ... };
get options() { ... }
// ... more stuff here ...
}
And it looks like this:
This is just 1 approach to implement such a more advanced component. At the end of the day it’s nice to have a function component (<.multi_select ... />
) that you can call without worrying (too much) about it’s implementation. It happens to be fully accessible also.
Only thing I’m not happy with is that it is stil bringing in quite a bit of javascript and I have to make sure the integration works well enough For example supporting grouped select options will be a bit more code. I have also not tested it in a morphdom setting (just single render for now).
I can also see the appeal of implementing such components via LiveView / LiveView.JS or Alpine.js. It will obviously result in more code when building it from the ground up but it won’t have a dependency on React/Node/etc.