Cartograph.Component (Cartograph v0.2.0)

View Source

This module provides functions for relative query patching and navigation event dispatching in templates.

Examples

Linkable Breadcrumbs

  use Phoenix.Component
  import Cartograph.Component, only: [parse_patch: 2]

  attr :trail, :list, required: true
  attr :curr_uri, :string, required: true
  attr :id, :string, required: true

  def breadcrumbs(assigns) do
    ~H"""
    <div id={@id}>
      <.link patch={
        parse_patch(@curr_uri, query: [remove: Enum.map(@trail, fn {p, _} -> p end)])
      }>
        Top
      </.link>
      <span>&nbsp;&gt;</span>
      <%= for {{query_param, display_label}, idx} <- Enum.with_index(@trail) do %>
        <span>
          <.link patch={
            parse_patch(@curr_uri,
              query: [
                remove: Enum.slice(@trail, (idx + 1)..-1//1) |> Enum.map(fn {p, _} -> p end)
              ]
            )
          }>
            {display_label}
          </.link>
          <span>&nbsp;&gt;</span>
        </span>
      <% end %>
    </div>
    """
  end

Old-School Pagination Widget

  use Phoenix.Component
  import Cartograph.Component, only: [cartograph_patch: 1]

  defp compute_prev_page(page_no, page_count) do
    if page_no == 1 do
      page_count
    else
      page_no - 1
    end
  end

  defp compute_next_page(page_no, page_count) do
    if page_no == page_count do
      1
    else
      page_no + 1
    end
  end

  attr :page_no, :integer, required: true
  attr :page_count, :integer, required: true
  attr :id, :string, required: true

  def pagination(assigns) do
    ~H"""
    <div id={@id}>
      <div>
        <button phx-click={
          cartograph_patch(
            query: [merge: %{page_no: compute_prev_page(@page_no, @page_count)}]
          )
        }>
          Prev
        </button>
        <p>Page {@page_no} of {@page_count}</p>
        <button phx-click={
          cartograph_patch(
            query: [merge: %{page_no: compute_next_page(@page_no, @page_count)}]
          )
        }>
          Next
        </button>
      </div>
      <div>
        <p>Jump to:</p>
        <input
          type="text"
          pattern="+"
          value={@page_no}
          phx-keydown={cartograph_patch(query: [merge: %{page_no: :phx_value}])}
          phx-key="Enter"
        />
      </div>
    </div>
    """
  end

Simple Stateful Selectable

  use Phoenix.Component
  import Cartograph.Component, only: [cartograph_patch: 1]

  attr :display_label, :string, required: true
  attr :choices, :list, required: true
  attr :selected, :string, required: true
  attr :query_param, :string, required: true
  attr :id, :string, required: true

  def generic_select(assigns) do
    ~H"""
    <label for={@id}>{@display_label}</label>
    <select id={@id}>
      <option value="" phx-click={cartograph_patch(query: [remove: [@query_param]])}>
        No Selection
      </option>
      <%= for {value, display_text} <- @choices do %>
        <option
          value={value}
          selected={@selected == value}
          phx-click={cartograph_patch(query: [merge: %{@query_param => value}])}
        >
          {display_text}
        </option>
      <% end %>
    </select>
    """
  end

Stateful Column Sort Button

Cycles through no sort > ascending > descending > no sort

  use Phoenix.Component
  import Cartograph.Component, only: [cartograph_patch: 1]

  defp sort_toggle(field_name, :asc = _curr_sort_order) do
    ops = [merge: %{"sort[]" => %{field_name <> "-asc" => field_name <> "-desc"}}]
    cartograph_patch(query: ops)
  end

  defp sort_toggle(field_name, :desc = _curr_sort_order) do
    cartograph_patch(query: [remove: %{"sort[]" => field_name <> "-desc"}])
  end

  defp sort_toggle(field_name, nil = _curr_sort_order) do
    cartograph_patch(query: [add: %{"sort[]" => field_name <> "-asc"}])
  end

  attr :display_text, :string, required: true
  attr :field_name, :string, required: true
  attr :sort_order, :atom, default: nil
  attr :sort_idx, :integer, default: nil

  def sort_button(assigns) do
    ~H"""
    <button class="common-btn" phx-click={sort_toggle(@field_name, @sort_order)}>
      <span>{@display_text}</span>
      <Heroicons.icon :if={@sort_order == :desc} name="arrow-down" class="inline-icon" />
      <Heroicons.icon :if={@sort_order == :asc} name="arrow-up" class="inline-icon" />
      <span if={@sort_order != nil} class="text-sm">{@sort_idx}</span>
    </button>
    """
  end

Summary

Types

A keyword list representing the query patching operations to apply to an existing URI.

Functions

Push a live navigation event to the server with the href computed by relative query parsing.

Push a live patch event to the server with the href computed by relative query parsing.

Parses a new URI suitable for use in a live navigation operation from the provided uri and opts.

Parses a new path suitable for use in a live patch operation from the provided uri and opts.

Types

query_opts()

@type query_opts() :: Keyword.t()

A keyword list representing the query patching operations to apply to an existing URI.

Unless otherwise specified in the specific option section, the values of the keyword items should be maps. Keys and values of the maps can be atoms or strings. Values can also be lists of atoms or strings in which case, the resulting query string will have one ocurrence of that key for each value in the list.

The operations are applied cumulatively in the order the keywords are provided, so the behavior of any arbitrary combination of operations is well-defined.

Providing a list as a value in the map will set multiple values for that key in the resulting query string. For example, given the map %{selected_role: [:admin, :member]}, this would be parsed out to: ?selected_role=admin&selected_role=member in the resulting query string. The exact semantics for how these values get applied depends on the operation being used.

Valid Options

  • :set - replaces the whole query relative to the current document with the key-value pairs provided.

    • Example with single-values:
      • base query: ?foo=bar
      • operation: query: [set: %{bar: :baz}]
      • result: ?bar=baz
    • Example with multi-values:
      • base query: ?foo=bar
      • operation: query: [set: %{bar: [:baz, :qux]}]
      • result: ?bar=baz&bar=qux
  • :add - appends the provided key-value pairs into the query string without checking for existing keys. This can create duplicates, which is needed for array-valued params.

    • Example with single-values:
      • base query: ?foo=bar
      • operation: query: [add: %{foo: :baz, bar: :baz}]
      • result: ?foo=bar&foo=baz&bar=baz
    • Example with multi-values:
      • base query: ?foo=bar
      • operation: query: [add: %{foo: [:baz, :qux], bar: :baz}]
      • result: ?foo=bar&foo=baz&foo=qux&bar=baz
  • :merge - the same as :add but replaces existing keys instead of appending duplicates.

    This option allows specifying a map as the value of a key in the top-level key-value map. If a regular scalar or list is provided as the value of the key, then all ocurrences of that key are removed and the provided values are added at the end of the query string. If a map is provided as the value for a key, it is used to match values to replace in-place for the corresponding key, so only params with matching values are removed and order of params is preserved.

    • Example with single-values:
      • base query: ?foo=bar
      • operation: query: [merge: %{foo: :baz, bar: :baz}]
      • result: ?foo=baz&bar=baz
    • Example with multi-values:
      • base query: ?foo=bar&bar=baz
      • operation: query: [merge: %{foo: [:baz, :qux], lorem: :ipsum}]
      • result: ?bar=baz&foo=baz&foo=qux&lorem=ipsum
    • Example with nested map-values:
      • base query: ?foo=bar&foo=baz&foo=spam&bar=baz
      • operation: query: [merge: %{foo: %{"spam" => :eggs, baz: "qux"}}]
      • result: ?foo=bar&foo=qux&foo=eggs&bar=baz
  • :remove - The opposite of :add. Removes matching keys.

    This option allows passing a list of keys instead of a map of key-value pairs. If a list of keys is provided, all ocurrences of the matched keys will be removed from the query string regardless of their value. If a map is provided, only the ocurrences of each key that have a matching value will be removed.

    • Example with keys only:
      • base query: ?foo=bar&foo=baz&bar=baz
      • operation: query: [remove: [:foo]]
      • result: ?bar=baz
    • Example with map single-values:
      • base query: ?foo=bar&foo=baz&bar=baz
      • operation: query: [remove: %{foo: :baz}]
      • result: ?foo=bar&bar=baz
    • Example with map multi-values:
      • base query: ?foo=bar&foo=baz&foo=qux&bar=baz
      • operation: query: [remove: %{foo: [:baz, :qux]}]
      • result: ?foo=bar&bar=baz
  • :toggle - toggles the provided key-value pairs into or out of the query string.

    This operation has the semantics of :add for any key-value pairs not in the current query and :remove for any that are in the current query.

    • Example with single-values:
      • base query: ?foo=bar
      • operation: query: [toggle: %{foo: :bar, bar: :baz}]
      • result: ?bar=baz
    • Example with multi-values:
      • base query: ?foo=bar
      • operation: query: [toggle: %{foo: [:bar, :baz], bar: :baz}]
      • result: ?foo=baz&bar=baz

Functions

cartograph_navigate(uri, opts \\ [])

Push a live navigation event to the server with the href computed by relative query parsing.

uri can be a URI.t/0 struct or a string:

  • cartograph_navigate(@cartograph_uri, query: [merge: %{page_no: 1}])
  • cartograph_navigate(~p"/users", query: [merge: %{page_no: 1}])

Options

  • :query - the query operations to apply, see: query_opts/0

    The special placeholder value :phx_value will be replaced by the current value of the input sending the event.

    • Example:
      • template: <input type="number" phx-key="Enter" phx-keydown={cartograph_navigate(@current_uri, query: [merge: %{"page_no" => :phx_value}])} />
      • current uri: https://localhost:4000/users?page_no=1
      • user input: 4
      • resulting navigate uri: https://localhost:4000/users?page_no=4
  • :loading - passed through to Phoenix.LiveView.JS.push/2 as-is.

  • :page_loading - passed through to Phoenix.LiveView.JS.push/2 as-is.

cartograph_patch(opts \\ [])

Push a live patch event to the server with the href computed by relative query parsing.

Options

  • :query - the query operations to apply, see: query_opts/0

    The special placeholder value :phx_value will be replaced by the current value of the input sending the event.

    • Example:
      • template: <input type="number" phx-key="Enter" phx-keydown={cartograph_patch(query: [merge: %{"page_no" => :phx_value}])} />
      • current uri: /users?page_no=1
      • user input: 4
      • resulting patch uri: /users?page_no=4
  • :loading - passed through to Phoenix.LiveView.JS.push/2 as-is.

  • :page_loading - passed through to Phoenix.LiveView.JS.push/2 as-is.

parse_navigate(uri, opts \\ [])

Parses a new URI suitable for use in a live navigation operation from the provided uri and opts.

uri can be a URI.t/0 struct or a string:

  • parse_navigate(@cartograph_uri, query: [merge: %{page_no: 1}])
  • parse_navigate(~p"/users", query: [merge: %{page_no: 1}])

Options

  • :query - the query operations to apply, see: query_opts/0
  • :phx_value - the value of this option will replace any ocurrences of :phx_value in the :query operations.
    • Example:
      • starting uri: https://localhost:4000/users?page_no=1
      • opts: query: [merge: %{"page_no" => :phx_value}], phx_value: 2
      • result: https://localhost:4000/users?page_no=2

parse_patch(uri, opts \\ [])

Parses a new path suitable for use in a live patch operation from the provided uri and opts.

uri can be a URI.t/0 struct or a string:

  • parse_patch(@cartograph_uri, query: [merge: %{page_no: 1}])
  • parse_patch(~p"/users", query: [merge: %{page_no: 1}])

Options

  • :query - the query operations to apply, see: query_opts/0
  • :phx_value - the value of this option will replace any ocurrences of :phx_value in the :query operations.
    • Example:
      • starting uri: /users?page_no=1
      • opts: query: [merge: %{"page_no" => :phx_value}], phx_value: 2
      • result: /users?page_no=2