A grid is a view of your app's rows; a document is text people write. A grid in a document (a Kotoba embed) is the rows themselves, live, among the text: people sort, filter and edit them where they read about them, and the document keeps only which grid and which view.
A table of the document (Kotoba's own) is the other thing: a few rows that exist only because of the document, with no source. If the rows would be there without the document, it is a grid.
The grid in a page
Three attributes make a grid a part of a page that scrolls:
<.data_grid {MyApp.Grids.tasks(@scope)} height="auto" max_height="24rem" wheel={:page} view={@view_params} />height="auto": the grid is as tall as its rows, up tomax_height(32rem unless given); past it, the rows scroll. Five open tasks take five rows of the page, not a box of 32rem.wheel={:page}: the wheel scrolls the page past the grid, until the person is in it (a click on it, Tab); then the wheel is the grid's. A grid alone on a page keeps the wheel (:grid, the default).view: the view the grid opens with, as a view's params (see Layout and views): its order, filters, search and columns. A saved view'sparams(Masu.Views) are such a view.
A grid in a Kotoba document
Kotoba keeps an embed's kind and ref, and your LiveView renders it
(Embeds). For a grid, a good ref
is a saved view: "Unpaid invoices over €1,000" is what a document talks
about, more than the invoices table.
<.kotoba field={@form[:body]} id="doc" embeds />
<.kotoba_embeds for="doc" label="Grid" icon="grid" readonly={@reading}>
<:embed :let={embed}>
<% view = MyApp.GridViews.get(embed.ref) %>
<.data_grid
:if={view}
{MyApp.Grids.for_view(view, @scope)}
id={"grid-" <> embed.id}
view={view.params}
height={if embed.full, do: "100%", else: "auto"}
wheel={if embed.full, do: :grid, else: :page}
changeset={if !embed.readonly, do: &MyApp.Tasks.Task.changeset/2}
/>
</:embed>
<:picker>
<.kotoba_embed_option :for={v <- @views} phx-click="insert_grid" phx-value-id={v.id} title={v.name} />
</:picker>
</.kotoba_embeds>/* A grid takes the height of its rows; a canvas keeps the embed's. */
.kotoba-embed[data-kind="grid"] .kotoba-embed-slot { height: auto; }- Give the grid an id of the embed's (
"grid-" <> embed.id): the same view can be in a document twice. - Check that the person may see the view the ref names, as for any of your pages: a document's refs are the person's input.
- Full screen (
embed.full) is the whole screen: the grid takes it all (height="100%") and the wheel. - A page that reads the document (
embed.readonly) shows the grid with no changeset: people sort and filter it, and change nothing. - Kotoba keeps the keys of an embed in it: the arrows, Enter and ⌘C are the grid's.
Markdown and HTML
A document exported to Markdown has its grid as a table of its rows, as
they are: a snapshot (Masu.Snapshot), which Markdown viewers draw, and
which Kotoba reads back as the grid (it keeps the embed's kind and ref
around it).
def render(%{kind: "grid", ref: ref}, format) when format in [:markdown, :html] do
view = MyApp.GridViews.get(ref)
sheet = Masu.Sheet.new(MyApp.Grids.for_view(view, scope))
case format do
:markdown -> Masu.Snapshot.markdown(sheet, view: view.params, rows: 20)
:html -> Masu.Snapshot.html(sheet, view: view.params, rows: 20)
end
end<!-- kotoba:embed {"kind":"grid","ref":"12","title":"Open tasks"} -->
| Task | Due | Done |
| :--- | :--- | :---: |
| Write the guide | 2026-11-02 | false |
_And 37 more rows._
<!-- /kotoba:embed -->The snapshot is the view's rows (its order, filters, search, and the
columns that show), at most rows of them (20 unless given), and a line
of how many more. A cell is its value as text; a cell of your own HEEx is
its column's value. An HTML snapshot is a table.masu-snapshot, for a
page or an email of the document.
Collaboration
A grid with issho is shared in its own room, whatever the document's:
its people are that room's, wherever they are. Give it its document, the
same as on the grid's own page, so that the two are one grid, and a
reader's can:
<.data_grid
...
issho={[instance: MyApp.Issho, document: "tasks:#{@account.id}/tasks", can: (embed.readonly && :read) || :write]}
user={@current_user}
/>Someone reading the document and someone on the grid's page see each other's cells and pointers. See Collaboration.