Add macro components and colocated hooks / js#3810
Merged
Conversation
Claude could have probably written it better than me.
SteffenDE
commented
May 23, 2025
SteffenDE
commented
May 27, 2025
SteffenDE
commented
May 27, 2025
SteffenDE
commented
May 27, 2025
SteffenDE
commented
May 27, 2025
SteffenDE
commented
May 27, 2025
SteffenDE
commented
May 27, 2025
SteffenDE
commented
May 27, 2025
SteffenDE
commented
May 27, 2025
SteffenDE
commented
May 27, 2025
|
This is great!!💯 thanks for bringing this to life. What's the rationale behind prefixing the hook name with a dot(.)? |
Collaborator
Author
|
@kamaroly it is to prevent conflicts with hook names, since it is likely that people won’t think a lot about naming their hooks when they are only used inside the same component. And since colocated hooks can also be used in libraries, it is quite likely that a some library names a hook very generically, and when you also do that things would break. |
|
Thank you for the clarification. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR introduces "macro components". Below is an excerpt from the (wip) documentation:
Phoenix.Component.MacroComponent
A macro component is a special type of component that can modify its content at compile time.
Instead of introducing a special tag syntax like
<#macro-component>, LiveView decided to "hide" macro components behind a special:typeattribute. The reason for this is that most of the time, the job of the macro component is to take its content and extract it so somewhere else, for example to a file in the local file system. A good example for this isPhoenix.LiveView.ColocatedHookandPhoenix.LiveView.ColocatedJS.AST
Macro components work by defining a callback module that implements the
Phoenix.LiveView.MacroComponentbehaviour. The module'sc:transform/2callback is called for each macro component used while LiveView compiles a HEEx component:In this example, the
ColocatedHook'sc:transform/2callback will be invoked with the AST of the<script>tag:The
Phoenix.Component.MacroComponent.ASTmodule provides some utilities to work with the AST, but in general it is quite simple:{tag, attributes, children}{key, value}tuples where the value is an Elixir AST (which can be a plain binary for simple attributes)Example: a compile-time markdown renderer
Let's say we want to create a macro component that renders markdown as HTML at
compile time. First, we need some library that actually converts the markdown to
HTML. For this example, we use
earmark.We start by defining the module for the macro component:
That's it. Since the div could contain nested elements, for example when using an HTML code block, we need to convert the children to a string first, using the
Phoenix.Component.MacroComponent.AST.to_string/1function.Then, we can simply replace the element's contents with the returned HTML string from Earmark.
We can now use the macro component inside our HEEx templates:
Note: this example uses the
proseclass from TailwindCSS for styling.One trick to prevent issues with extra whitespace is that we use a
<pre>tag in the LiveView template, which prevents thePhoenix.LiveView.HTMLFormatterfrom indenting the contents, which would mess with the markdown parsing. When rendering, we replace it with a<div>tag in the macro component.Colocated Hooks / Colocated JavaScript
When writing components that require some more control over the DOM, it often feels inconvenient to have to write a hook in a separate file. Instead, one wants to have the hook logic right next to the component code. For such cases, HEEx supports macro components, and LiveView comes with two implementations of macro components by default:
Phoenix.LiveView.ColocatedHookandPhoenix.LiveView.ColocatedJS.Let's see an example:
When LiveView finds a
<script>element with:type={ColocatedHook}, it will extract the hook code at compile time and write it into a special folder inside the_build/directory. To use the hooks, all that needs to be done is to import the manifest into your JS bundle, which is automatically done in theapp.jsfile generated bymix phx.new:... import {Socket} from "phoenix" import {LiveSocket} from "phoenix_live_view" import topbar from "../vendor/topbar" + import colocated from "phoenix-colocated/my_app" let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") let liveSocket = new LiveSocket("/live", Socket, { longPollFallbackMs: 2500, params: {_csrf_token: csrfToken}, + hooks: {...colocated.hooks} })The
"phoenix-colocated"package is a JavaScript package inside theMix.Project.build_path(), which is included by default in theesbuildconfiguration of new Phoenix projects:TODO: fix path separator for windows!!!
When rendering a component that includes a colocated hook, the
<script>tag is omitted from the rendered output. Furthermore, to prevent conflicts with other components, colocated hooks require you to use the special dot syntax when naming the hook, as well as in thephx-hookattribute. LiveView will prefix the hook name by the current module name at compile time. This also means that in cases where a hook is meant to be used in multiple components across a project, the hook must be defined as a regular, non-colocated hook instead.You can read more about colocated hooks in the module documentation for
ColocatedHook. LiveView also supports colocating other JavaScript code, for more information, seePhoenix.LiveView.ColocatedJS.