Phoenix function component for Xenia's Monaco adapter.
Wraps the editor value into the versioned payload expected by the JavaScript Monaco adapter.
Examples
import Xenia.Adapters.Monaco
<.monaco
id="editor"
value={@code}
version={@version}
language="elixir"
on_change="code_changed"
style="height: 400px"
/>As a form field, the id, the input name and the initial value come from the field, and the editor's text is submitted with the form:
<.form for={@form} phx-change="validate" phx-submit="save">
<.monaco field={@form[:code]} language="elixir" />
</.form>The Monaco JavaScript adapter must be registered with createHooks()
under the name given in adapter (default "Monaco"). It comes from its
own entry point, and your app passes the Monaco import:
import { createMonaco } from "xenia/monaco"
createHooks({ Monaco: createMonaco({ load: () => import("monaco-editor") }) })Setup
Monaco is the heaviest library to set up, so here is all of it.
1. Install it: npm install monaco-editor@0.57.0 --save-exact --prefix assets
2. Create the adapter in assets/js/monaco.js. Monaco finds its web
workers with new URL(..., import.meta.url), which esbuild doesn't
bundle, so tell it where they are. The import("monaco-editor") is
written in your app, so esbuild finds it in your assets/node_modules:
import { createMonaco } from "xenia/monaco"
const WORKERS = {
json: "json", css: "css", scss: "css", less: "css",
html: "html", handlebars: "html", razor: "html",
typescript: "ts", javascript: "ts",
}
self.MonacoEnvironment = {
getWorker: (_id, label) => new Worker(`/assets/js/monaco/${WORKERS[label] ?? "editor"}.worker.js`),
}
export const monaco = createMonaco({ load: () => import("monaco-editor") })Register it next to your other adapters: createHooks({ Monaco: monaco }).
To add completion providers or commands, pass
setup: (monaco, editor) => disposable to createMonaco.
3. Build the workers in config/config.exs: give app.js an entry
name, add the five worker files, and a loader for Monaco's icon font:
config :esbuild,
my_app: [
args: ~w(app=js/app.js
monaco/editor.worker=node_modules/monaco-editor/esm/vs/editor/editor.worker.js
monaco/json.worker=node_modules/monaco-editor/esm/vs/language/json/json.worker.js
monaco/css.worker=node_modules/monaco-editor/esm/vs/language/css/css.worker.js
monaco/html.worker=node_modules/monaco-editor/esm/vs/language/html/html.worker.js
monaco/ts.worker=node_modules/monaco-editor/esm/vs/language/typescript/ts.worker.js
--bundle --target=es2022 --outdir=../priv/static/assets/js
--external:/fonts/* --external:/images/* --alias:@=. --loader:.ttf=file),
# cd and env as Phoenix generated them
]Restart mix phx.server after changing config.exs.
4. Load Monaco's CSS. esbuild writes the CSS Monaco imports next to
app.js. Add a second stylesheet to root.html.heex:
<link phx-track-static rel="stylesheet" href={~p"/assets/js/app.css"} />5. Use it, with import Xenia.Adapters.Monaco in html_helpers, and
echo the client's version back from your change handler. That's how the
editor tells your echo of its own typing apart from a real update:
def handle_event("code_changed", %{"value" => code, "version" => version}, socket) do
{:noreply, assign(socket, code: code, version: version)}
endPass version={nil} for an authoritative update. Commands:
Xenia.push_to(socket, "editor", "focus"), and "insert" with
%{text: "..."}.
Theme
The editor follows the app's theme provider (light → vs, dark →
vs-dark). themes replaces those defaults with your own, which can be
theme maps. theme pins one theme and ignores the app's:
<.monaco id="editor" themes={%{dark: %{name: "midnight", base: "vs-dark",
colors: %{"editor.background" => "#0b1020"}}}} />
<.monaco id="editor" theme="hc-black" />Monaco themes are global: the last theme applied wins for every editor on the page.
Forcing the server's value
Props are only sent to the client when they change. If the server rejects
an edit and re-renders the same value it rendered before, nothing is sent
and the editor keeps the user's text. Increment revision whenever the
server's value must replace the editor's content, for example when a form
is reset after Save.
Summary
Functions
Renders a Monaco editor through Xenia.
Functions
Renders a Monaco editor through Xenia.
Attributes
id(:string) - stable DOM id; commands use it. Defaults to the field's id. Defaults tonil.field(Phoenix.HTML.FormField) - Phoenix form field, for example @form[:code]; provides id and value. Defaults tonil.adapter(:string) - adapter name registered in createHooks(). Defaults to"Monaco".value(:string) - editor content when no field is given; nil is treated as "". Defaults tonil.version(:integer) - version echoed from a change event; nil means the value is authoritative. Defaults tonil.revision(:integer) - increment to force the server's value, even if it equals the last one rendered. Defaults to0.language(:string) - Monaco language id; omitted if nil. Defaults tonil.theme(:any) - explicit Monaco theme override (a name or a theme map); beats the app's theme. Omitted if nil. Monaco themes are global, so this affects every editor on the page. Defaults tonil.themes(:map) - the Monaco theme for each app theme, e.g. %{light: "vs", dark: %{name: "midnight", base: "vs-dark", colors: %{...}}}. App themes it leaves out use the defaults (light → vs, dark → vs-dark). Omitted if nil. Defaults tonil.options(:map) - additional Monaco editor options; must not contain value, language or theme. Defaults to%{}.on_change(:string) - LiveView event pushed when the content changes. Defaults tonil.shortcuts(:map) - Key combination => LiveView event, while the editor has focus (see Xenia.Shortcuts). Defaults to%{}.on_crash(:string) - LiveView event sent if the editor throws (see Xenia.Component). Defaults tonil.- Global attributes are accepted.
Slots
loading- Shown until the editor has loaded, e.g. "Loading the editor…".error- Shown if the editor fails; data-xenia-retry starts it again.