{# Grid component (GridComponent in components.py, request half in grid.py; usage docs in manual/grid.org). There is exactly *one* rendering of the table below, and everything else is layered on it: * Without JavaScript it is readable and scrollable, sorted and paged by plain links, filtered by a GET form, and edited by row forms — a ✎ link re-renders one row as inputs, the blank bottom row adds, a × button deletes. All state is query-string keys namespaced by the component id (``-page``, ``-sort``, ``-edit``, ``-f-``), so two grids share a page without reading each other's state. * With JavaScript, static/js/construct-grid.min.js hydrates the same markup into a spreadsheet: it reads the JSON in [data-fcu-grid-config], edits cells in place against the row endpoints, and scrolls more rows in. It never rebuilds the table, which is why the two renderings cannot drift. The no-JS controls carry ``js-hide`` so the two never both appear: the library's compiler hides (and disables) them once scripting is present. Forms live *outside* the table and the controls inside it reference them by ``form="…"`` — a
cannot be a child of . One row form serves both editing and adding (only one is ever open); one delete form serves every row, each button carrying its own row id as its submitted value. #} {% macro cell_input(field, name, value, form_id, choices, error, input_types) %} {#- The no-JS twin of buildEditor() in frontend/src/grid/cells.js: same widget per field type, so the row form and the in-cell editor never disagree about what a value looks like while it is being typed. -#} {%- if field.field_type == 'bool' -%} {%- elif choices is not none -%} {%- elif field.field_type == 'text' -%} {%- else -%} {%- endif -%} {%- if error %}{{ error }}{% endif -%} {% endmacro %} {% if writable and form_action %} {# The row form: update when a row is open for editing, create otherwise. Only one of the two rows is ever rendered, so one form serves both. #} {{ csrf_field }} {% if edit_row %}{% endif %} {% if spec.can_delete %}
{{ csrf_field }}
{% endif %} {% endif %} {% if spec.filterable %}
{% for key, value in carry_args %} {% endfor %}
{% endif %} {% if config %} {# Read once at hydration. A parse failure leaves the table exactly as the server rendered it, which is a working read-only grid. ``tojson`` rather than plain interpolation: a " in a cell value able to close the element. #} {{ csrf_field }} {% endif %}
{% if spec.caption %}{% endif %} {% for name, field in spec.columns %} {% endfor %} {% if writable %}{% endif %} {% if spec.filterable %} {% for name, field in spec.columns %} {% endfor %} {% if writable %} {% endif %} {% endif %} {% for row in rows %} {% if edit_row and row.id|string == edit_row.id|string %} {% for cell in row.cells %} {% endfor %} {% else %} {% for cell in row.cells %} {% endfor %} {% if writable %} {% endif %} {% endif %} {% else %} {% if not spec.columns %} {% endif %} {% endfor %} {% if new_row %} {# The blank add row. Hidden once scripting is present: the client maintains its own, which inserts one cell at a time. #} {% for name, field in spec.columns %} {% endfor %} {% endif %}
{{ spec.caption }}
{{ sort_links[name].label }}{% if sort_links[name].dir == 'asc' %} {% elif sort_links[name].dir == 'desc' %} {% endif %}
{{ cell_input(cell.field, cell.name, edit_row.prefill.get(cell.name), gid ~ '-rowform', choices.get(cell.name), edit_row.errors.get(cell.name), input_types) }} {{ t.cancel }}
{{ cell.text }} {% if spec.can_delete %} {% endif %} {# Where the client puts its own delete button. It needs a slot of its own because the cell is not empty: the no-JS controls above are still here, merely hidden. #}
{{ t['no-columns'] }}
{{ cell_input(field, name, new_row.prefill.get(name), gid ~ '-rowform', choices.get(name), new_row.errors.get(name), input_types) }}