Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions guides/actions/item-actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ In the above example, we only return the `show` item action. This way we replace

An item action is a module that uses the `Backpex.ItemAction` module. To get started, you can use the `BackpexWeb` module and provide the `:item_action` option. This will import the necessary functions and macros to define an item action.

If the item action needs your application's Gettext backend, components, or verified routes, you can additionally `use MyAppWeb, :html`. Both `use` orders are supported.

In the following example, we define an item action to navigate to the show view of a user.

```elixir
Expand Down
12 changes: 12 additions & 0 deletions guides/fields/custom-fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ The `render_form/1` function returns markup that is used to render a form on `ed

See `Backpex.Field` for more information on the available callback functions. For example, you can implement `render_index_form/1` to make the field editable in the index view.

## Using your application's web helpers

Custom fields are LiveComponents. If a field needs helpers from your application, such as Gettext, core components, or verified routes, configure your application's LiveComponent entrypoint:

```elixir
use Backpex.Field,
config_schema: @config_schema,
live_component: {MyAppWeb, :live_component}
```

The standard Phoenix-generated `MyAppWeb, :live_component` entrypoint includes the application's HTML helpers while setting up `Phoenix.LiveComponent` exactly once. Do not additionally `use MyAppWeb, :html` in the same field module.

## Add field option validation

With Backpex v0.9 we are validating field options. This ensures that only field options that are actually used by the field can be defined in the field options map. So if your custom field requires certain field options, make sure you define them.
Expand Down
2 changes: 2 additions & 0 deletions guides/filter/custom-filter.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Backpex ships with a set of default filters that can be used to filter the data.

You can create a custom filter by using the `filter` macro from the `BackpexWeb` module. It automatically implements the `Backpex.Filter` behavior and defines some aliases and imports.

If the filter needs your application's Gettext backend, components, or verified routes, you can additionally `use MyAppWeb, :html`. This also works with the built-in filter macros such as `use Backpex.Filters.Select`, and both `use` orders are supported.

### Required Callbacks

When creating a custom filter, you need to implement the following callbacks:
Expand Down
44 changes: 44 additions & 0 deletions guides/upgrading/v0.19.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,47 @@ defmodule MyAppWeb.PostLive do
end
end
```

## Backpex extension modules now import `Phoenix.Component`

The `BackpexWeb` entrypoints for item actions, filters, and metrics now import
`Phoenix.Component` instead of using it. This avoids registering the
declarative `@before_compile` hook twice when an extension also uses its application's
HTML entrypoint.

You can now use application-level Gettext, components, and verified routes directly in
these extensions, in either order:

```elixir
defmodule MyAppWeb.ItemActions.Archive do
use BackpexWeb, :item_action
use MyAppWeb, :html

# ...
end
```

This also applies to custom filters using `BackpexWeb, :filter` or one of
`Backpex.Filters.Select`, `Backpex.Filters.MultiSelect`, `Backpex.Filters.Boolean`, and
`Backpex.Filters.Range`, as well as metrics using `BackpexWeb, :metric`.

No change is required for extensions that only use `~H` and the helpers provided by
Backpex. If an item action, filter, or metric declares its own function components with
`attr` or `slot`, it must now additionally `use MyAppWeb, :html` or
`use Phoenix.Component`.

## Custom fields can use the host LiveComponent entrypoint

`Backpex.Field` still needs the complete `Phoenix.LiveComponent` setup. To make your
application's Gettext backend, components, and verified routes available without using
`Phoenix.Component` twice, configure the host LiveComponent entrypoint:

```elixir
use Backpex.Field,
config_schema: @config_schema,
live_component: {MyAppWeb, :live_component}
```

The configured entrypoint must set up a `Phoenix.LiveComponent`, as the standard
Phoenix-generated `MyAppWeb, :live_component` entrypoint does. It replaces Backpex's
default LiveComponent setup, so do not additionally `use MyAppWeb, :html` in the field.
48 changes: 46 additions & 2 deletions lib/backpex/field.ex
Original file line number Diff line number Diff line change
Expand Up @@ -226,15 +226,35 @@ defmodule Backpex.Field do

@doc """
Defines `Backpex.Field` behaviour and provides default implementations.

A custom field can use its application's LiveComponent entrypoint to make
application helpers such as Gettext and verified routes available:

use Backpex.Field,
config_schema: @config_schema,
live_component: {MyAppWeb, :live_component}

The configured entrypoint must set up a `Phoenix.LiveComponent`, as the standard
Phoenix-generated `MyAppWeb, :live_component` entrypoint does.
"""
defmacro __using__(opts) do
quote bind_quoted: [opts: opts] do
opts = Macro.expand(opts, __CALLER__)

if !Keyword.keyword?(opts) do
raise ArgumentError, "expected Backpex.Field options to be a keyword list, got: #{Macro.to_string(opts)}"
end

{live_component, opts} = Keyword.pop(opts, :live_component)
live_component = live_component(live_component, __CALLER__)

quote bind_quoted: [opts: opts], unquote: true do
@config_schema opts[:config_schema] || []

@before_compile Backpex.Field
@behaviour Backpex.Field

use BackpexWeb, :field
unquote(live_component)
use BackpexWeb, :field_helpers

@doc """
Returns the schema of configurable options for this field.
Expand Down Expand Up @@ -262,6 +282,30 @@ defmodule Backpex.Field do
end
end

defp live_component(nil, _caller) do
quote do
use Phoenix.LiveComponent
end
end

defp live_component({module, entrypoint}, caller) when is_atom(entrypoint) do
module = Macro.expand(module, caller)

if !is_atom(module) do
raise ArgumentError,
"expected :live_component to contain a module, got: #{Macro.to_string(module)}"
end

quote do
use unquote(module), unquote(entrypoint)
end
end

defp live_component(live_component, _caller) do
raise ArgumentError,
"expected :live_component to be a {module, entrypoint} tuple, got: #{inspect(live_component)}"
end
Comment thread
Copilot marked this conversation as resolved.

defmacro __before_compile__(_env) do
quote generated: true do
import Ecto.Query
Expand Down
1 change: 1 addition & 0 deletions lib/backpex/filters/boolean.ex
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ defmodule Backpex.Filters.Boolean do
> In addition it will add a `render` and `render_form` function in order to display the corresponding filter.
> It will also implement the `Backpex.Filter.query` function to define a boolean query.
"""
use Phoenix.Component
use BackpexWeb, :filter

@doc """
Expand Down
1 change: 1 addition & 0 deletions lib/backpex/filters/multi_select.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ defmodule Backpex.Filters.MultiSelect do
>
> When you `use Backpex.Filters.MultiSelect`, the `Backpex.Filters.MultiSelect` module will set `@behavior Backpex.Filters.Select`. In addition it will add a `render` and `render_form` function in order to display the corresponding filter.
"""
use Phoenix.Component
use BackpexWeb, :filter

import Backpex.HTML.CoreComponents
Expand Down
1 change: 1 addition & 0 deletions lib/backpex/filters/range.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ defmodule Backpex.Filters.Range do
> In addition it will add a `render` and `render_form` function in order to display the corresponding filter.
> It will also implement the `Backpex.Filter.query` function to define a range query.
"""
use Phoenix.Component
use BackpexWeb, :filter

require Backpex
Expand Down
1 change: 1 addition & 0 deletions lib/backpex/filters/select.ex
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ defmodule Backpex.Filters.Select do
> When you `use Backpex.Filters.Select`, the `Backpex.Filters.Select` module will set `@behavior Backpex.Filters.Select`.
> In addition it will add a `render` and `render_form` function in order to display the corresponding filter.
"""
use Phoenix.Component
use BackpexWeb, :filter

@doc """
Expand Down
1 change: 1 addition & 0 deletions lib/backpex/metrics/value.ex
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ defmodule Backpex.Metrics.Value do
end
"""

use Phoenix.Component
use BackpexWeb, :metric

attr :metric, :any, required: true, doc: "the metric to be rendered"
Expand Down
44 changes: 30 additions & 14 deletions lib/backpex_web.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,24 @@ defmodule BackpexWeb do
end
end

@doc """
Includes the functions and helpers needed to render HEEx without setting up a
`Phoenix.Component` module.

This entrypoint is intended for Backpex extension modules that may additionally
`use MyAppWeb, :html`. Importing `Phoenix.Component` provides the `~H` sigil and
component helpers without registering another declarative `@before_compile` hook.
"""
def heex do
quote do
import Phoenix.Component

require Phoenix.Component.Declarative

unquote(html_helpers())
end
end

@doc """
Includes the functions and helpers available inside a `Backpex.LiveResource`.

Expand All @@ -40,13 +58,7 @@ defmodule BackpexWeb do
# the module additionally brings in the component-aware `def` (via `use MyAppWeb, :html`),
# regardless of the order of the `use` statements.
def live_resource do
quote do
import Phoenix.Component

require Phoenix.Component.Declarative

unquote(html_helpers())
end
heex()
end

@doc """
Expand All @@ -56,6 +68,13 @@ defmodule BackpexWeb do
quote do
use Phoenix.LiveComponent

unquote(field_helpers())
end
end

@doc false
def field_helpers do
quote do
alias Backpex.HTML
alias Backpex.HTML.Form, as: BackpexForm
alias Backpex.HTML.Layout
Expand All @@ -71,35 +90,32 @@ defmodule BackpexWeb do
"""
def item_action do
quote do
use Phoenix.Component
use Backpex.ItemAction

import Phoenix.LiveView

alias Backpex.Router

unquote(html_helpers())
unquote(heex())
end
end

def filter do
quote do
use Phoenix.Component

import Backpex.HTML.Form, only: [error: 1]
import Ecto.Query, warn: false

unquote(html_helpers())
unquote(heex())
end
end

def metric do
quote do
@behaviour Backpex.Metric

use Phoenix.Component

import Ecto.Query

unquote(heex())
end
end

Expand Down
39 changes: 39 additions & 0 deletions test/backpex/web_helpers_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
defmodule Backpex.WebHelpersTest do
use ExUnit.Case, async: false

@fixture Path.expand("../fixtures/web_helpers/extension_modules.fixture", __DIR__)

@fixture_modules [
Backpex.WebHelpersTest.Helpers,
Backpex.WebHelpersTest.Web,
Backpex.WebHelpersTest.ItemActionBackpexFirst,
Backpex.WebHelpersTest.ItemActionWebFirst,
Backpex.WebHelpersTest.FilterBackpexFirst,
Backpex.WebHelpersTest.FilterWebFirst,
Backpex.WebHelpersTest.SelectFilterBackpexFirst,
Backpex.WebHelpersTest.SelectFilterWebFirst,
Backpex.WebHelpersTest.MetricBackpexFirst,
Backpex.WebHelpersTest.MetricWebFirst,
Backpex.WebHelpersTest.Field
]

test "Backpex extensions compile with host web helpers without warnings" do
purge_fixture_modules()
on_exit(&purge_fixture_modules/0)

assert {:ok, modules, %{compile_warnings: [], runtime_warnings: []}} =
Kernel.ParallelCompiler.compile([@fixture],
max_concurrency: 1,
return_diagnostics: true
)

assert Enum.sort(modules) == Enum.sort(@fixture_modules)
end

defp purge_fixture_modules do
Enum.each(@fixture_modules, fn module ->
:code.purge(module)
:code.delete(module)
end)
end
end
Loading
Loading