# `MishkaGervaz.Table.Web.State`
[🔗](https://github.com/mishka-group/mishka_gervaz/blob/v0.0.5/lib/mishka_gervaz/table/web/state.ex#L1)

Single state struct for MishkaGervaz table.

All table state is held in this struct rather than in separate assigns.

## State shape

State is split into two parts:
- `static` - Configuration that never changes (same reference for O(1) comparison)
- Dynamic fields - User interaction state that triggers re-renders

This separation allows LiveView to skip re-rendering static parts (columns, filters, etc.)
when only dynamic state (page, filter_values, etc.) changes.

The state holds no rows. A load's rows reach the page through the table's stream, and
`records_result` is the `Phoenix.LiveView.AsyncResult` of the last load: `%{page: page,
empty?: boolean}` of the load that started the list once it succeeds, which Load more and Load
earlier keep, failed with the error once a load fails. Only a table with
`keep_loaded_records true` keeps its loaded rows, in `loaded_records`. `after_keyset` is the keyset
of the last row a load read, the one string Load more reads after when the read action allows
keyset pagination. On a read action without keyset, Load more on a table that keeps no rows
reads from `next_offset` instead: the end of the rows the view shows, one less for each row taken
out of the list. `removed_ids` holds the ids of the rows the view took out itself until their own
realtime removal arrives, so each is counted once.

`hidden_ids` holds the picked rows, of `selected_ids`, that the table's read no longer finds, such
as rows a search hides. A bulk action reaches only the rows the read finds, so the bulk bar counts
the picked rows without them and says how many the filters hide. A load that starts the list
afresh while rows are picked finds them in one read of their ids.

`page` is the last page the view shows, the one the URL holds. `first_page` is the first: a
`:load_more` or `:infinite` table opened at page N, as a reload after Load more opens it, shows
page N alone, and Load earlier reads the page before it, one page a click, after the keyset of
the first row shown, `before_keyset`. A table that keeps its rows reads from the first page, so
its `first_page` is 1.

## Sub-builders

State initialization is composed of sub-builders that can be overridden:

- `ColumnBuilder` - Builds columns from DSL and resource
- `FilterBuilder` - Builds filters from DSL and resource
- `ActionBuilder` - Builds row/bulk actions
- `Presentation` - Resolves UI adapter and templates
- `UrlSync` - Handles URL state synchronization
- `Access` - Handles access control

## User Override

Override the entire state module:

    defmodule MyApp.Table.State do
      use MishkaGervaz.Table.Web.State

      def init(id, resource, user) do
        state = super(id, resource, user)
        %{state | custom_field: :value}
      end
    end

Override specific sub-builders:

    defmodule MyApp.Table.State do
      use MishkaGervaz.Table.Web.State,
        column: MyApp.Table.ColumnBuilder,
        filter: MyApp.Table.FilterBuilder
    end

Or override via DSL:

    mishka_gervaz do
      table do
        state do
          column MyApp.Table.ColumnBuilder
          filter MyApp.Table.FilterBuilder
        end
      end
    end

Override entire state module via DSL:

    mishka_gervaz do
      table do
        state module: MyApp.Table.CustomState
      end
    end

## Helper Functions

Helper functions are available in `MishkaGervaz.Table.Web.State.Helpers` and can be
used when overriding state functions:

    defmodule MyApp.Table.State do
      use MishkaGervaz.Table.Web.State
      alias MishkaGervaz.Table.Web.State.Helpers, as: StateHelpers

      def hydrate_relation_filter_labels(state) do
        # Use helpers in your override
        StateHelpers.hydrate_filter(filter, acc, state)
      end
    end

See `MishkaGervaz.Table.Web.State.Helpers`,
`MishkaGervaz.Table.Web.State.ColumnBuilder`,
`MishkaGervaz.Table.Web.State.FilterBuilder`,
`MishkaGervaz.Table.Web.State.ActionBuilder`,
`MishkaGervaz.Table.Web.State.Presentation`,
`MishkaGervaz.Table.Web.State.UrlSync`,
`MishkaGervaz.Table.Web.State.Access`,
`MishkaGervaz.Table.Web.AutoState`,
`MishkaGervaz.Table.Web.Refresh`,
`MishkaGervaz.Table.Web.UrlSync`.

# `archive_status`

```elixir
@type archive_status() :: :active | :archived
```

# `loading_status`

```elixir
@type loading_status() :: :initial | :loading | :loaded | :error
```

# `loading_type`

```elixir
@type loading_type() :: :initial | :reset | :more | :earlier
```

# `t`

```elixir
@type t() :: %MishkaGervaz.Table.Web.State{
  after_keyset: String.t() | nil,
  archive_status: archive_status(),
  base_path: String.t() | nil,
  before_keyset: String.t() | nil,
  current_page_size: pos_integer() | nil,
  current_user: map() | nil,
  dismissed_notices: MapSet.t(),
  excluded_ids: MapSet.t(any()),
  expanded_data: struct() | nil,
  expanded_id: String.t() | nil,
  filter_values: map(),
  first_page: pos_integer(),
  has_initial_data?: boolean(),
  has_more?: boolean(),
  hidden_ids: MapSet.t(any()),
  loaded_records: list(),
  loading: loading_status(),
  loading_type: loading_type(),
  master_user?: boolean(),
  next_offset: non_neg_integer(),
  page: integer(),
  path_params: map(),
  preload_aliases: %{required(atom()) =&gt; atom()},
  preserved_params: map(),
  records_result: Phoenix.LiveView.AsyncResult.t(),
  relation_filter_state: %{required(atom()) =&gt; map()},
  removed_ids: MapSet.t(),
  saved_active_state: map() | nil,
  saved_archived_state: map() | nil,
  select_all?: boolean(),
  selected_ids: MapSet.t(any()),
  sort_fields: [{atom(), :asc | :desc}],
  static: MishkaGervaz.Table.Web.State.Static.t(),
  supports_archive: boolean(),
  template: module(),
  total_count: integer() | nil,
  total_pages: integer() | nil
}
```

# `apply_presentation`

Applies a mount's own presentation choices over the resource's — see the `__using__` version.

# `apply_url_state`

```elixir
@spec apply_url_state(t(), map() | nil) :: t()
```

# `bidirectional_url_sync?`

```elixir
@spec bidirectional_url_sync?(t()) :: boolean()
```

# `can_modify_record?`

```elixir
@spec can_modify_record?(t(), map()) :: boolean()
```

# `default_init`

```elixir
@spec default_init(String.t(), module(), map() | nil) :: t()
```

# `get_action`

```elixir
@spec get_action(t(), atom()) :: atom()
```

# `get_preloads`

```elixir
@spec get_preloads(t()) :: [atom()]
```

# `hydrate_relation_filter_labels`

```elixir
@spec hydrate_relation_filter_labels(t()) :: t()
```

Hydrate relation filter state with labels for selected values from URL

# `init`

```elixir
@spec init(String.t(), module(), map() | nil) :: t()
```

# `record_visible?`

```elixir
@spec record_visible?(t(), map()) :: boolean()
```

# `switch_template`

```elixir
@spec switch_template(t(), atom()) :: {:ok, t()} | {:error, :template_not_allowed}
```

# `template_switching_enabled?`

```elixir
@spec template_switching_enabled?(t()) :: boolean()
```

# `update`

```elixir
@spec update(t(), keyword() | map()) :: t()
```

---

*Consult [api-reference.md](api-reference.md) for complete listing*
