Allow app web helpers in Backpex extensions - #2170
Conversation
There was a problem hiding this comment.
Pull request overview
This PR improves how Backpex extension modules (item actions, filters, metrics, and custom fields) access host-application web helpers (Gettext/components/verified routes) while avoiding duplicate Phoenix.Component.Declarative @before_compile hooks that can fail compilation under --warnings-as-errors.
Changes:
- Introduce a
BackpexWeb, :heexentrypoint and update:item_action,:filter, and:metricto importPhoenix.Component(instead ofuse), preventing duplicate declarative hooks when combined withMyAppWeb, :html. - Add a
live_component: {MyAppWeb, :live_component}option toBackpex.Fieldso custom fields can reuse the host LiveComponent setup (and helpers) exactly once. - Add compiler-verification fixtures/tests plus documentation and upgrade notes covering the new usage patterns and required
attr/slotsetup.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| test/fixtures/web_helpers/extension_modules.fixture | Compile fixture covering both use orders and host helper availability across extensions. |
| test/backpex/web_helpers_test.exs | Test that compiling the fixture produces no compile/runtime diagnostics. |
| lib/backpex/metrics/value.ex | Ensures Backpex’s built-in value metric keeps full Phoenix.Component setup for attr. |
| lib/backpex/filters/select.ex | Ensures built-in select filter keeps full Phoenix.Component setup for attr. |
| lib/backpex/filters/range.ex | Ensures built-in range filter keeps full Phoenix.Component setup for attr. |
| lib/backpex/filters/multi_select.ex | Ensures built-in multi-select filter keeps full Phoenix.Component setup for attr. |
| lib/backpex/filters/boolean.ex | Ensures built-in boolean filter keeps full Phoenix.Component setup for attr. |
| lib/backpex/field.ex | Adds :live_component option handling so custom fields can use host LiveComponent entrypoint. |
| lib/backpex_web.ex | Adds :heex + :field_helpers entrypoints and switches extension entrypoints to use :heex. |
| guides/upgrading/v0.19.md | Upgrade notes explaining the behavior change and required actions for attr/slot. |
| guides/filter/custom-filter.md | Docs for using host :html in custom/built-in filter macros and supported use orders. |
| guides/fields/custom-fields.md | Docs for configuring host LiveComponent entrypoint in custom fields. |
| guides/actions/item-actions.md | Docs for using host :html helpers in item actions (either use order). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
| end | ||
| ``` | ||
|
|
||
| ## Backpex extension modules now import `Phoenix.Component` |
| ```elixir | ||
| use Backpex.Field, | ||
| config_schema: @config_schema, | ||
| live_component: {MyAppWeb, :live_component} |
There was a problem hiding this comment.
Does this fail on compile or on render? It should fail on compile.
(To be honest, I’m not a big fan of the syntax. Feels very much like a workaround. But I can’t think of anything better at the moment.)
| if !Keyword.keyword?(opts) do | ||
| raise ArgumentError, "expected Backpex.Field options to be a keyword list, got: #{Macro.to_string(opts)}" | ||
| end |
There was a problem hiding this comment.
🤖
What changed. Before the PR, Backpex.Field.__using__ passed the options through quote bind_quoted:, so whatever expression the caller wrote was evaluated inside the caller's module body at compile time. Any expression that produced a keyword list worked. The PR now inspects the options as syntax tree at macro-expansion time: it calls Macro.expand on them, requires the result to literally be a keyword list, pops :live_component out of it, and raises otherwise. Macro.expand only expands macros at the top level. It never evaluates attributes, variables, or function calls, so anything that isn't a written-out [key: value, ...] fails the check.
Shapes that break now. Each of these compiled fine before and now raises at compile time:
use Backpex.Field, @optswith the options stored in a module attribute. The AST expands to an attribute read, not a list. This is the case I verified.use Backpex.Field, Keyword.merge(@base, config_schema: ...)or any function call producing the options.use Backpex.Field, optswhereoptsis a variable bound earlier in the module body.- A wrapper macro in the app or a library that forwards options through
bind_quoted. That pattern injects a variable at the innerusesite, so a base module like this now
fails:
defmacro __using__(opts) do
quote bind_quoted: [opts: opts] do
use Backpex.Field, opts # opts is a variable here, not a literal list
end
endShapes that still work. A literal keyword list whose values are expressions is fine, because only the outer list shape is inspected. use Backpex.Field, config_schema: @config_schema keeps working, as does a macro that expands directly into a keyword list. All built-in fields and the documented usage are in this group, so most users won't hit it.
Why it matters anyway. The failure is a compile error with a misleading message. Someone with a shared base field module sees expected Backpex.Field options to be a keyword list, got: Module.__get_attribute__(__MODULE__, :opts, 3, true) after a routine upgrade, with no hint that the fix is to inline the options.
Fix. Only pop :live_component when the options are a literal keyword list, and otherwise pass them through untouched with the default use Phoenix.LiveComponent. That restores the old behaviour for every non-literal case. The only limitation is that :live_component itself must be given literally, which is reasonable since it has to be resolved at compile time to emit a use.
Summary
live_component: {MyAppWeb, :live_component}Phoenix.Componentsetup in Backpex's built-in filters and value metric because they declareattruseorders and host helpers under compiler verificationWhy
PR #2055 fixed duplicate
Phoenix.Component.Declarative@before_compilehooks for LiveResources. The same duplicate hook was still introduced byBackpexWeb, :item_action,:filter, and:metricwhen consumers also usedMyAppWeb, :html.Fields need a different path because they are real LiveComponents. The new option lets the host application's standard
:live_componententrypoint perform the LiveComponent setup exactly once while Backpex adds only its field-specific aliases and helpers.User impact
Item actions, custom filters, typed filters, and metrics may now use
MyAppWeb, :htmlin either order to access application helpers. Extensions that declare their ownattrorslotmust explicitly use their application HTML entrypoint orPhoenix.Component.Custom fields can use:
Existing fields without this option keep the current Backpex LiveComponent setup.
Checks
MIX_ENV=test mix lint— 275 tests passed (100 doctests, 175 tests), Credo cleanmix test test/backpex/web_helpers_test.exs— passed with no compile or runtime diagnostics