Combo.Inertia.Conn (combo_inertia v2.0.0)

Copy Markdown View Source

%Plug.Conn{} helpers for rendering Inertia responses.

Summary

Functions

Marks a prop as "always", which means it will be always included in the props.

Enable (or disable) automatic conversion of prop keys from snake case (e.g. inserted_at), which is conventional in Elixir, to camel case (e.g. insertedAt), which is conventional in JavaScript.

Instructs the client-side to clear the history state.

Marks a prop as "deep_merge", which means it will be deeply merged with existing data on the client-side.

Marks a prop as "defer", which means it will be loaded after initial page render.

Instructs the client-side to encrypt the current page's data before pushing it to the history state.

Forces the Inertia client side to perform a redirect. This can be used as a plug or inline when building a response.

Marks a prop as "merge", which means it will be merged with existing data on the client-side.

Marks a prop as "once", which is cached on the client-side and reused on subsequent pages that include the same prop.

Marks a prop as "optional", which means it will only get evaluated when explicitly requested in a partial reload.

Assigns errors to the Inertia page data. This helper accepts any data that implements the Combo.Inertia.Errors protocol. By default, this library implements error serializers for Ecto.Changeset and bare maps.

Puts a prop to the Inertia page data.

Renders an Inertia response.

Determines if a response has been rendered with Inertia.

Marks a prop for infinite scroll pagination. It automatically configures merge behavior for the data key and extracts pagination metadata for the client side <InfiniteScroll> component.

Prevents auto-transformation of a prop key to camel-case (when camelize_props is enabled).

Types

always()

@opaque always()

component()

@type component() :: String.t()

deep_merge()

@opaque deep_merge()

defer()

@opaque defer()

merge()

@opaque merge()

once()

@opaque once()

optional()

@opaque optional()

preserved_prop_key()

@opaque preserved_prop_key()

prop_key()

@type prop_key() :: raw_prop_key() | preserved_prop_key()

prop_value()

@type prop_value() :: any()

props()

@type props() :: map()

raw_prop_key()

@type raw_prop_key() :: atom() | String.t()

render_opt()

@type render_opt() :: {:ssr, boolean()}

render_opts()

@type render_opts() :: [render_opt()]

scroll()

@opaque scroll()

Functions

inertia_always(value)

@spec inertia_always(value :: any()) :: always()

Marks a prop as "always", which means it will be always included in the props.

Examples

# ALWAYS included on standard visits
# ALWAYS included on partial reloads
# ALWAYS evaluated
inertia_put_prop(conn, :users, inertia_always(Users.all()))

inertia_camelize_props(conn)

@spec inertia_camelize_props(Plug.Conn.t()) :: Plug.Conn.t()

Enable (or disable) automatic conversion of prop keys from snake case (e.g. inserted_at), which is conventional in Elixir, to camel case (e.g. insertedAt), which is conventional in JavaScript.

Examples

Using camelize_props here will convert first_name to firstName in the response props.

conn
|> inertia_put_prop(:first_name, "Bob")
|> inertia_camelize_props()
|> inertia_render("Home")

You may also pass a boolean to the camelize_props function (to override any previously-set or globally-configured value):

conn
|> inertia_put_prop(:first_name, "Bob")
|> inertia_camelize_props(false)
|> inertia_render("Home")

inertia_camelize_props(conn, value)

@spec inertia_camelize_props(Plug.Conn.t(), boolean()) :: Plug.Conn.t()

inertia_clear_history(conn)

@spec inertia_clear_history(Plug.Conn.t()) :: Plug.Conn.t()

Instructs the client-side to clear the history state.

inertia_clear_history(conn, value)

@spec inertia_clear_history(Plug.Conn.t(), boolean()) :: Plug.Conn.t()

inertia_deep_merge(value)

@spec inertia_deep_merge(prop_value()) :: deep_merge()

Marks a prop as "deep_merge", which means it will be deeply merged with existing data on the client-side.

inertia_defer(fun)

@spec inertia_defer(fun()) :: defer()

Marks a prop as "defer", which means it will be loaded after initial page render.

Examples

inertia_put_prop(conn, :users, inertia_defer(fn -> Users.all() end))
inertia_put_prop(conn, :users, inertia_defer(fn -> Users.all() end, "group1"))

inertia_defer(fun, group)

@spec inertia_defer(fun(), String.t()) :: defer()

inertia_encrypt_history(conn)

@spec inertia_encrypt_history(Plug.Conn.t()) :: Plug.Conn.t()

Instructs the client-side to encrypt the current page's data before pushing it to the history state.

inertia_encrypt_history(conn, value)

@spec inertia_encrypt_history(Plug.Conn.t(), boolean()) :: Plug.Conn.t()

inertia_force_redirect(conn, opts \\ [])

@spec inertia_force_redirect(Plug.Conn.t(), opts :: keyword()) :: Plug.Conn.t()

Forces the Inertia client side to perform a redirect. This can be used as a plug or inline when building a response.

This plug modifies the response to be a 409 Conflict response and include the destination URL in the x-inertia-location header, which will cause the Inertia client to perform a window.location = url visit.

Note: we automatically convert regular external redirects (via the Combo redirect helper), but this function is useful if you need to force redirect to a non-external route that is not handled by Inertia.

See https://inertiajs.com/redirects#external-redirects

Examples

conn
|> inertia_force_redirect()
|> redirect(to: "/non-inertia-powered-page")

inertia_merge(value)

@spec inertia_merge(prop_value()) :: merge()

Marks a prop as "merge", which means it will be merged with existing data on the client-side.

inertia_once(fun, opts \\ [])

@spec inertia_once(
  fun_or_tagged :: fun() | defer() | merge() | deep_merge() | optional(),
  opts :: keyword()
) :: once()

Marks a prop as "once", which is cached on the client-side and reused on subsequent pages that include the same prop.

Options

  • :fresh - When true, forces the prop to be refreshed ignoring the client cache. Also accepts a boolean condition. Defaults to false.
  • :until - Sets an expiration time. Accepts a DateTime or integer seconds from now.
  • :as - Custom key for sharing data across pages with different prop names.

Examples

# Basic once prop
inertia_put_prop(conn, :plans, inertia_once(fn -> Plan.all() end))

# Forcing a refresh
inertia_put_prop(conn, :plans, inertia_once(fn -> Plan.all() end, fresh: true))

# Expires in 1 hour
inertia_put_prop(conn, :rates, inertia_once(
  fn -> ExchangeRate.current() end,
  until: 3600
))

# Expires at a specific time
inertia_put_prop(conn, :rates, inertia_once(
  fn -> ExchangeRate.current() end,
  until: DateTime.utc_now() |> DateTime.add(1, :day)
))

# With custom key for sharing across pages
inertia_put_prop(conn, :member_roles, inertia_once(fn -> Role.all() end, as: "roles"))

# Combined options
inertia_put_prop(conn, :plans, inertia_once(fn -> Plan.all() end,
  fresh: should_refresh?,
  until: DateTime.utc_now() |> DateTime.add(1, :day),
  as: "billing_plans"
))

# Combining with other prop types
inertia_put_prop(conn, :permissions, inertia_once(inertia_defer(fn -> Permission.all() end)))

inertia_optional(fun)

@spec inertia_optional(fun()) :: optional()

Marks a prop as "optional", which means it will only get evaluated when explicitly requested in a partial reload.

Examples

// NEVER included on standard visits
// OPTIONALLY included on partial reloads
// ONLY evaluated when needed
inertia_put_prop(conn, :users, inertia_optional(fn -> Users.all() end))

inertia_put_errors(conn, data)

@spec inertia_put_errors(Plug.Conn.t(), data :: term()) :: Plug.Conn.t()

Assigns errors to the Inertia page data. This helper accepts any data that implements the Combo.Inertia.Errors protocol. By default, this library implements error serializers for Ecto.Changeset and bare maps.

If you are serializing your own errors maps, they should take the following shape:

%{
  "name" => "Name is required",
  "password" => "Password must be at least 5 characters",
  "team.name" => "Team name is required",
}

When assigning a changeset, you may optionally pass a message-generating function to use when traversing errors. See Ecto.Changeset.traverse_errors/2 for more information about the message function.

defp default_msg_fun({msg, opts}) do
  Enum.reduce(opts, msg, fn {key, value}, acc ->
    String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end)
  end)
end

This default implementation performs a simple string replacement for error message containing variables, like count. For example, given the following error:

{"should be at least %{count} characters", [count: 3, validation: :length, min: 3]}

The generated description would be "should be at least 3 characters". If you would prefer to use the Gettext module for pluralizing and localizing error messages, you can override the message function:

conn
|> inertia_put_errors(changeset, fn {msg, opts} ->
  if count = opts[:count] do
    Gettext.dngettext(MyApp.Web.Gettext, "errors", msg, msg, count, opts)
  else
    Gettext.dgettext(MyApp.Web.Gettext, "errors", msg, opts)
  end
end)

inertia_put_errors(conn, data, msg_fun)

@spec inertia_put_errors(Plug.Conn.t(), data :: term(), msg_fun :: function()) ::
  Plug.Conn.t()

inertia_put_prop(conn, key, value)

@spec inertia_put_prop(Plug.Conn.t(), prop_key(), prop_value()) :: Plug.Conn.t()

Puts a prop to the Inertia page data.

Examples

# ALWAYS included on standard visits
# OPTIONALLY included on partial reloads
# ALWAYS evaluated
inertia_put_prop(conn, :users, Users.all())

# ALWAYS included on standard visits
# OPTIONALLY included on partial reloads
# ONLY evaluated when needed
inertia_put_prop(conn, :users, fn -> Users.all() end)

inertia_render(conn, component)

@spec inertia_render(Plug.Conn.t(), component()) :: Plug.Conn.t()

Renders an Inertia response.

Options

  • ssr: whether to server-side render the response (see the docs on "Server-side rendering" in the README for more information on setting this up). Defaults to the globally-configured value, or false if no global config is specified.

Examples

conn
|> inertia_put_prop(:user_id, 1)
|> inertia_render("SettingsPage")

You may pass additional props as map for the third argument:

conn
|> inertia_put_prop(:user_id, 1)
|> inertia_render("SettingsPage", %{name: "Bob"})

You may also pass options for the last positional argument:

conn
|> inertia_put_prop(:user_id, 1)
|> inertia_render("SettingsPage", ssr: true)

conn
|> inertia_put_prop(:user_id, 1)
|> inertia_render("SettingsPage", %{name: "Bob"}, ssr: true)

inertia_render(conn, component, props)

@spec inertia_render(Plug.Conn.t(), component(), props() | render_opts()) ::
  Plug.Conn.t()

inertia_render(conn, component, props, opts)

@spec inertia_render(Plug.Conn.t(), component(), props(), render_opts()) ::
  Plug.Conn.t()

inertia_response?(conn)

@spec inertia_response?(Plug.Conn.t()) :: boolean()

Determines if a response has been rendered with Inertia.

inertia_scroll(value, opts \\ [])

@spec inertia_scroll(value :: any(), opts :: keyword()) :: scroll()

Marks a prop for infinite scroll pagination. It automatically configures merge behavior for the data key and extracts pagination metadata for the client side <InfiniteScroll> component.

Options

  • :wrapper - The key containing the data items. Default to "data".
  • :page_name - Override the page query parameter name.
  • :metadata - Custom metadata extraction function.

Examples

# Basic usage with auto-detected metadata
inertia_put_prop(conn, :users, inertia_scroll(paginated_users))

# With lazy evaluation
inertia_put_prop(conn, :users, inertia_scroll(fn -> Users.paginate(opts) end))

# Custom wrapper key
inertia_put_prop(conn, :users, inertia_scroll(data, wrapper: "items"))

# Custom metadata
inertia_put_prop(conn, :users, inertia_scroll(data, metadata: fn data ->
  %{page_name: "p", current_page: 1, next_page: 2, previous_page: nil}
end))

preserve_case(key)

@spec preserve_case(raw_prop_key()) :: preserved_prop_key()

Prevents auto-transformation of a prop key to camel-case (when camelize_props is enabled).

Example

conn
|> inertia_put_prop(preserve_case(:this_will_not_be_camelized), "value")
|> inertia_put_prop(:this_will_be_camelized, "another_value")
|> inertia_camelize_props()
|> inertia_render("Home")

You can also use this helper inside of nested props:

conn
|> inertia_put_prop(:user, %{
  preserve_case(:this_will_not_be_camelized) => "value",
  this_will_be_camelized: "another_value"
})
|> inertia_camelize_props()
|> inertia_render("Home")