The render-time bridge between a compiled runtime template and a compiled runtime component.
Every module built by MishkaCmsCore.Runtime.Compilers.ComponentCompiler imports component/1
and component/2 from here, so a template written as <.component component_name="card" .../>
resolves against an already-loaded module rather than through an Code.eval_quoted/3
interpretation. Which resolution it takes is the module's own answer: a function component is
apply(CardModule, :card, [assigns]), and one compiled stateful goes through
Phoenix.Component.live_component/1 instead. The template is identical either way — see the fork
in applied/3 for why the call site must not change.
MishkaCmsCore.Runtime.StaticHelpers is this module for the surfaces that render a template to a
string, where a live component has no diff to be turned into HTML by.
Slot items arriving from the database (:runtime_slot) are grouped by their for_slot name and
resolved against the target module's declared slots, so a name the component never declared
is reported as a slot error instead of silently minting an atom.
Which site's components a render reaches
A global component (site="Global", or no site) and a component of the site this process renders
for — the one MishkaCmsCore.Runtime.Translations.put_site/1 named — and nothing else. A component
reads as its own site, so one named from another site's page would draw that site's template and
that site's rows to every visitor, and run its handle_event. It is refused before it is built and
nothing is drawn in its place. A process that names no site reaches the global ones only. The
module that answers is asked as well, since two names can hash to one module: it is drawn only
when it was built from a row of the site asked for, or from a global row.
The site is named where a request becomes a page — MishkaCmsWeb.RuntimeLive,
MishkaCmsWeb.DraftPreviewLive, MishkaCmsWeb.ErrorHTML — and by the builder's canvas; a preview
built for one site among others renders inside rendering_for/2. A global layout draws on the
page's site, so it reaches that site's components and the global ones.
In a task
The site a template renders for and the response's CSP nonce are kept in the process rendering
it, as Gettext's locale is. A task the template starts through
MishkaGervaz.Messages.start_async/4 — or assign_async/4, or in_caller_locale/1 — is handed
all three (carry_into_tasks/0), so t/1,2, site_path/2, component/1 and csp_nonce/0
answer inside it what they answer outside it:
def load(socket) do
MishkaGervaz.Messages.start_async(socket, :links, fn ->
%{about: site_path("/:lang/about", "fa"), label: t("About")}
end)
endA task started any other way, Phoenix.LiveView.start_async/4 among them, renders for no site
and in the default locale: site_path/2 is nil there, and t/1 answers from the global words.
Events
runtime:render:component_unavailable— what is drawn in place of a refused component, an empty%Phoenix.LiveView.Rendered{}, with%{component_name: name, site: site, site_id: site_id}as its meta: the name and site the template asked for, and the site being rendered. A plugin may answer with markup of its own.runtime:render:before_content_component— before each component arich_content/1marker names is drawn; seeMishkaCmsCore.Runtime.ContentComponents.
See also MishkaCmsCore.Runtime.Compilers.ComponentCompiler for how the target modules and their
__component_information__/0 metadata surface are generated.
Summary
Functions
The captcha a form asks — see MishkaCmsCore.Captcha.Widget.captcha/1.
Registers what the imports above read from the process rendering a template — the site
MishkaCmsCore.Runtime.Translations.put_site/1 names and the nonce
MishkaCmsCore.Runtime.CspNonce.put/1 records — with
MishkaGervaz.Messages.carry_process_keys/1, so a task started through
MishkaGervaz.Messages.start_async/4, assign_async/4 or in_caller_locale/1 renders as its
starter does. Gervaz carries the locale itself. See "In a task" above.
Renders a runtime component by name.
Renders a runtime component from a name/site pair, merging any extra assigns.
This response's Content-Security-Policy nonce, for the rare <style> a template cannot avoid —
a page, a layout and a component alike, since a component never sees the page's assigns.
Builds the runtime module for name/site if it is not already loaded, and says whether it is.
What a runtime template imports from here — component/1,2, captcha/1, sign_in_providers/1,
rich_content/1,
t/1,2, csp_nonce/0 and site_path/2 — less any name its own helpers already define.
Runs fun rendering for the site named site, then puts back the site this process rendered for.
Whether a template rendered by this process may reach a component of the site named site.
The module component/1 draws for name of the site named site, built on first use — or
:error where it draws nothing: a name nothing answers to, an inactive component, another site's.
Stored HTML with the components its markers name drawn in place — a post's body, say.
The buttons of the services the site signs in with — see
MishkaCmsCore.Accounts.SignInProvider.Widget.sign_in_providers/1.
The public address of a stored path on the site this page is rendered for, in a language or with
captures — MishkaCmsCore.Runtime.Languages.url/3, so a link is written once and follows the
site's URL mode
The same component, rendered as MARKUP rather than as something LiveView will run — what the builder's canvas draws.
key in the language this page is being answered in — the runtime counterpart of gettext/1,
for templates that live in the database and so can never reach a .po file.
Functions
The captcha a form asks — see MishkaCmsCore.Captcha.Widget.captcha/1.
<.captcha form={f} for="sign_in" tenant={assigns[:current_tenant]} />
@spec carry_into_tasks() :: :ok
Registers what the imports above read from the process rendering a template — the site
MishkaCmsCore.Runtime.Translations.put_site/1 names and the nonce
MishkaCmsCore.Runtime.CspNonce.put/1 records — with
MishkaGervaz.Messages.carry_process_keys/1, so a task started through
MishkaGervaz.Messages.start_async/4, assign_async/4 or in_caller_locale/1 renders as its
starter does. Gervaz carries the locale itself. See "In a task" above.
Called once, when MishkaCmsCore.Application starts.
Renders a runtime component by name.
Requires :component_name; :site selects the tenant-scoped module and may be nil for a
global component. Any other assign is forwarded to the component untouched.
A :site that is neither global nor the site this process renders for draws nothing and builds
nothing — see "Which site's components a render reaches" above.
A component whose precompile flag is unset is not built at boot, so it is built when a render
first needs it. The page or layout drawing it builds the components it reaches several at once
before it draws (MishkaCmsCore.Runtime.ComponentPrefetch), and one it could not tell it
reaches is built here — by the first render that names it. MishkaCmsCore.Runtime.Handlers.ModuleCompilerHandler
coordinates through its registry, so concurrent first renders of the same component wait on one
compile rather than racing, and a component whose template does not compile is remembered in
MishkaCmsCore.Runtime.States.CompileFailureState so the next render short-circuits to the
placeholder instead of retrying.
The component's CSS does not depend on this: MishkaCmsCore.Runtime.Handlers.BootHandler writes
a template file for every active component whether or not it compiles a module, and that file is
what the closure walk and Tailwind read. So a lazily-built component renders styled the first
time it is seen.
Renders a runtime component from a name/site pair, merging any extra assigns.
@spec csp_nonce() :: String.t() | nil
This response's Content-Security-Policy nonce, for the rare <style> a template cannot avoid —
a page, a layout and a component alike, since a component never sees the page's assigns.
<style nonce={csp_nonce()}>…</style>nil outside a response, which HEEx renders as no attribute. See
MishkaCmsCore.Runtime.CspNonce for where the value comes from.
Builds the runtime module for name/site if it is not already loaded, and says whether it is.
component/1 calls this itself. It is public for the callers that read a component's compiled
metadata before rendering it — the page builder canvas asks __component_information__/1 which
attributes a component declares, and a component that has never been built declares nothing, so
every attribute an author set would be silently dropped from the preview.
@spec imports([map()] | nil) :: keyword(non_neg_integer())
What a runtime template imports from here — component/1,2, captcha/1, sign_in_providers/1,
rich_content/1,
t/1,2, csp_nonce/0 and site_path/2 — less any name its own helpers already define.
Every compiler — component, page and layout — asks this rather than writing the list itself, so
the three cannot drift. The exception is not hypothetical: importing a t/1 into a module that
defines one is a compile error, and a template that saved yesterday would stop saving today. Its
own function wins, as a local function would anyway.
Runs fun rendering for the site named site, then puts back the site this process rendered for.
site is a name, as a <.component site="…"> attribute carries one. "Global", nil and a name
SiteState does not know render for no site, which reaches the global components alone.
For a process that renders one site's template and then another's — a preview built in the admin,
a palette card — where naming the site for the whole process would leave it pointing at whichever
was drawn last. t/1 inside fun answers from the same site's words.
Whether a template rendered by this process may reach a component of the site named site.
"Global" and nil always may; a site name only when it is the site
MishkaCmsCore.Runtime.Translations.put_site/1 named for this process. A name SiteState does
not know never may. Two ETS reads for a site name, none for a global one.
component/1 and static_component/1 ask it themselves. It is public for a caller that builds a
component before rendering it — the builder's canvas reads a component's declared attributes
first — so that a component the render would refuse is never built, and its row never read, on
the way there.
The module component/1 draws for name of the site named site, built on first use — or
:error where it draws nothing: a name nothing answers to, an inactive component, another site's.
For a caller that must know before it draws, as rich_content/1 does to cast a marker's
attributes to what the component declares.
@spec rich_content(map()) :: Phoenix.LiveView.Rendered.t()
Stored HTML with the components its markers name drawn in place — a post's body, say.
<.rich_content
id={"#{@id}-body"}
html={@body}
site={site_name()}
current_user={@current_user}
/>html— sanitized HTML; it is drawn as it is given, markers aside.site— the site's name, as<.component site="…">takes it. A marker names the site's own component, else the global one of that name.nilreaches the global ones alone.id— what a stateful component's id starts with,"content"when not given; unique on the page when two of these draw content.current_user— handed to a component that declares acurrent_userattribute.
A marker that names nothing this site may draw is drawn as nothing. See
MishkaCmsCore.Runtime.ContentComponents for the marker and its rules.
The buttons of the services the site signs in with — see
MishkaCmsCore.Accounts.SignInProvider.Widget.sign_in_providers/1.
<.sign_in_providers site={assigns[:current_site]} />
The public address of a stored path on the site this page is rendered for, in a language or with
captures — MishkaCmsCore.Runtime.Languages.url/3, so a link is written once and follows the
site's URL mode:
<.link navigate={site_path("/:lang/about", @page.language)}>About</.link>
<a href={site_path("/:lang/blog/:slug", %{"lang" => @page.language, "slug" => @post.slug})}>A language of nil is the site's primary one. nil when the path makes no address — a capture
it needs is missing, or the language is not one the site publishes in — which HEEx draws as no
href at all. The site is the one MishkaCmsCore.Runtime.Translations.put_site/1 named for this
process, as for component/1.
The same component, rendered as MARKUP rather than as something LiveView will run — what the builder's canvas draws.
Why the canvas cannot call component/1
MishkaCmsCoreWeb.Live.Admin.Builder.CanvasLive.contain/3 forces every node's render to iodata on
purpose, so that a component's raise, throw or exit lands inside a rescue the editor owns rather
than taking an author's unsaved session with it. Phoenix.LiveView.Engine implements
Phoenix.HTML.Safe for %Phoenix.LiveView.Component{} only to raise — the two facts are
irreconcilable, and contain/3 is the one that must win.
So a stateful component is rendered here the way LiveView would, minus the process: its mount/1
runs if it has one, the compiler's update_many/1 carries the author's assigns across, and
render/1 is called as the ordinary arity-1 function it is. Real markup, real size, real styling.
What the author sees, and what they do not
@myself is nil, so phx-target renders away rather than pointing at a cid that does not exist —
which means nothing in the frame responds to a click. That is the honest picture: the canvas is
showing the component's COLD state, and the editor says so beside the selection rather than
leaving the reader to wonder why the dropdown will not open.
The socket built here carries private[:runtime_cold], which is how a component that loads in the
background knows to read inline instead: there is no process to send a task's answer to. That is
the ONLY surface where it is set — a page's first HTTP render is not connected either, and a
component must not confuse the two.
A function component takes the same path it always did, and another site's component is refused
here exactly as component/1 refuses it.
key in the language this page is being answered in — the runtime counterpart of gettext/1,
for templates that live in the database and so can never reach a .po file.
<h2>{t("Categories")}</h2>
<p>{t("Read %{count} posts", count: @count)}</p>A key nobody has translated renders as itself. The value is a plain string, escaped like any
other. See MishkaCmsCore.Runtime.Translations.translate/2.