Lists and Tables

web_foreach renders a row template once per item of a list and gives every row a data-key, so a later action can replace or remove exactly that row. Wire its html into a custom pin of the render block and print it with triple braces.

A table in three parts

A search, a row template and a page template. The row knows nothing about the page around it, which is what lets an action render the same row again later.

The flow
blueprint "Products admin"

flow page = web_page with {event: "admin/products", scopes: ["*loggedin"]} {
  found = search@products(query: query_sort_field[name](asc: true), limit: 100)
  let list = web_foreach(items: found.array, template = "{{> product-row}}", empty = "<tr><td colspan='3'>No products yet.</td></tr>")
  web_render(page.connection,
    rows: list.html,
    total: list.count,
    title = "Products",
    template = "{{> products-page}}"
  ) with {custom_pins: [{id: "rows", state: "rows", type: "value"}, {id: "total", state: "total", type: "number"}]}
}
Row template (stored as product-row)
<tr>
  <td>{{name}}</td>
  <td>{{price | money "EUR"}}</td>
  <td>
    <button type="button" data-action="edit">Edit</button>
    <button type="button" data-action="delete" data-confirm="Delete {{name}}?" data-optimistic="remove">Delete</button>
  </td>
</tr>
Page template (stored as products-page)
<h1>{{title}} ({{total}})</h1>
<table>
  <tbody id="products">{{{rows}}}</tbody>
</table>

Each <tr> arrives with its key, for example <tr data-key="a23575…">. Actions shows the delete and edit flows those buttons run.

The pins of web for each

It is a data block with no flow pins. It runs when something reads its html, so put a search in the flow before it.

PinIs
items (in)The list to render, such as the array of a function_search. Required.
template (in)The template of one row. Leave it unwired to use the template stored on the block.
key (in)The item field that identifies a row, as a dotted path if needed.
empty (in)A template to render instead when there are no items.
data (in)An object whose fields every row can read too. Custom pins do the same, one value at a time.
html (out)All rows joined, ready to print.
count (out)How many rows were rendered, 0 for an empty list.
success, error (out)A template error gives an empty html, success false and the message on error. The flow carries on, so check it if the list matters.

A row reads its own fields first

Inside a row, the item is the context, so a document's name is {{name}}. Behind it are the data fields, the custom pins and web. A row can read a shared value such as {{currency}} as long as the item has no field of that name.

The loop variables work as in any loop: {{@index}}, {{@number}}, {{@first}}, {{@last}}, {{@odd}}, {{@even}} and {{@length}}. For a list of plain values, the value itself is {{.}}.

Every row gets a data-key

The key goes on the row's first element. This is the order it is decided in:

SituationThe key is
The row's first element already has data-keyThat one. It is kept.
The key pin names a fieldThat field's value. If the item has no such value, the row's position.
No key pinThe first of _meta.guid, guid, id and key the item has. A stored document therefore gets its guid.
None of thoseThe row's position, counting from 0.
  • A row without a first element, plain text or text in front of a tag, is wrapped in a <div data-key="...">. Start table rows with their <tr>, as a <div> does not belong in a table.
  • Keys must be unique on the page. An update finds its element by key, and the first match wins. Mind this when one page shows two lists of the same documents.
  • A position is only a fallback. Positions move when a row is removed, so give rows that actions change a real key.

A button inside a row sends that row's key along, and the action receives it on its key out-pin. The closest element with a data-key decides.

Print the rows with triple braces

The output is finished HTML, with every value in it already escaped. Normal braces would escape it again and show the tags to the visitor.

Write

<tbody id="products">{{{rows}}}</tbody>

Not

<tbody id="products">{{rows}}</tbody> prints the markup as text.

An empty list has its own template

With an empty list, or an items value that is not a list, the block renders the empty template with the shared data and count is 0. Without an empty template the result is an empty string. The empty template gets no key, as there is no item to take one from.

Use web for each for rows that change

web_foreach

Rows an action changes later. It keys every row, its row template can be rendered again by an action, and it has count and empty.

{{for}} in the template

Anything that only shows: the items of an order, a list of tags, a list inside a row. It is less wiring, but the rows have no key unless you write one with data-key="{{item.id}}".

Load more is an insert

The list above stops after one limit. For the next rows, a button carries the offset and the action adds rows to the end of the list. Add a custom pin next with the value 100 to the render block, and put this below the table:

Below the table, and stored as load-more
<div id="more">{{> load-more}}</div>

{{! The stored template load-more: }}
<button type="button" data-action="more" data-param-offset="{{next}}" data-loading-text="Loading">Load more</button>
The action
flow more = web_action with {event: "more"} {
  let {offset} = more.form
  found = search@products(query: query_sort_field[name](asc: true), limit: 100, offset: offset)
  web_insert(
    target = "#products",
    html: web_foreach(items: found.array, template = "{{> product-row}}")
  )
  web_update(
    target = "#more",
    html: web_template(next: number_plus(offset, 100), template = "{{> load-more}}") with {custom_pins: [{id: "next", state: "next", type: "number"}]}
  )
}

web_insert appends by default, and web_update gives the button its next offset. Once found.hits is below the limit there is nothing more to load: branch on that and answer with a web_remove of #more instead.

Next steps

Actions Replace, remove and insert the rows you just rendered. Storages Searching, creating and updating the documents a page shows and its actions change. Layouts and Partials Store the row template so a page and an action share it.