Conditions and repetition

Show or hide content with && and ?:, repeat rows with .map, filter and count lists, and lay out grids, all against the validated input.

Conditions and loops use the JavaScript forms models already write. The compiler recognizes each one and turns it into data, so the renderer decides which branch to show and how many rows to repeat when a message is sent.

templates/digest.tsx
import { defineTemplate } from "@samva/markup";
import { Column, Columns, Email, Section } from "@samva/markup/email";
import { jsonSchema } from "@samva/markup/input-schema";

export default defineTemplate({
  id: "digest",
  schema: jsonSchema<{
    name: string;
    vip: boolean;
    steps: { title: string; done: boolean }[];
    deals: { title: string; price: string }[];
  }>({
    type: "object",
    properties: {
      name: { type: "string" },
      vip: { type: "boolean" },
      steps: {
        type: "array",
        items: {
          type: "object",
          properties: { title: { type: "string" }, done: { type: "boolean" } },
          required: ["title", "done"],
          additionalProperties: false,
        },
      },
      deals: {
        type: "array",
        items: {
          type: "object",
          properties: { title: { type: "string" }, price: { type: "string" } },
          required: ["title", "price"],
          additionalProperties: false,
        },
      },
    },
    required: ["name", "vip", "steps", "deals"],
    additionalProperties: false,
  }),
  fixtures: {
    default: {
      name: "Ada",
      vip: true,
      steps: [
        { title: "Verify email", done: true },
        { title: "Add a domain", done: false },
      ],
      deals: [
        { title: "Shirt", price: "$42" },
        { title: "Hat", price: "$10" },
        { title: "Scarf", price: "$18" },
      ],
    },
    empty: { name: "Grace", vip: false, steps: [], deals: [] },
  },
  email: {
    subject: (input) => `${input.name}, your weekly digest`,
    body: (input) => (
      <Email>
        <Section className={input.vip ? "bg-amber-100" : "bg-white"}>
          {input.vip ? <p>Thanks for being a VIP.</p> : <p>Upgrade for early access.</p>}
          {input.steps.filter((step) => !step.done).length > 0 && (
            <p>{input.steps.filter((step) => !step.done).length} steps left</p>
          )}
          <ul>
            {input.steps.map((step) => (
              <li>{step.title}</li>
            ))}
          </ul>
          <Columns each={input.deals} per={2}>
            {(deal) => (
              <Column>
                <p>{deal.title}</p>
                <p>{deal.price}</p>
              </Column>
            )}
          </Columns>
        </Section>
      </Email>
    ),
  },
});
  • condition && <Element /> shows the element only when the condition holds.
  • condition ? <A /> : <B /> chooses between two elements.
  • Conditions combine ===, !==, <, >, <=, >=, !, &&, ||, and .length.
  • list.map((item) => <Row />) repeats the row for each item.
  • list.filter((item) => condition) narrows a list before .map or .length. The condition uses the same grammar as && and ?:.
  • <Columns each={list} per={n}> lays a list out in rows of n cells.
  • className={condition ? "a" : "b"} chooses between literal class lists.

The editor shows one fixture at a time, so switch fixtures to see the other branch of a condition, and add a fixture for each branch and for an empty list. Editing a repeated row once changes every item. The condition and the list a loop reads stay in source, or go to the assistant: ask it to "only show the returns section for orders over $100".

Anything that would need a variable, a reduce, or index arithmetic is computed by the caller and sent in the payload. See the static profile.

Related documentation