Lattice Grid Buy a licence

demo D284

A feed enriched from a lookup

One order feed joined to a customer lookup, held until known and released enriched

addSource(join: { from, localKey, fields, missing: 'hold' })

Building…
Loading a live grid…

The configuration

'data-router-join': () => ({
  rows: [], config: {},
  foot: [
    'one order feed enriched from a customer lookup, joined by the data router',
    'an order whose customer is not known yet is held, not shown half-filled',
    'load the customers and the held orders release already carrying the name and tier',
  ],
  mount: (el: HTMLElement, LG: any) => {
    const money = { style: 'currency', currency: 'USD', decimals: 0 } as const;
    const TIER_VARIANTS = { map: { Gold: 'warning', Silver: 'neutral', Bronze: 'info' } };
    const CUSTOMERS = [
      { id: 'C1', kind: 'customer', name: 'Northwind', tier: 'Gold' },
      { id: 'C2', kind: 'customer', name: 'Contoso', tier: 'Silver' },
      { id: 'C3', kind: 'customer', name: 'Fabrikam', tier: 'Gold' },
      { id: 'C4', kind: 'customer', name: 'Adventure Works', tier: 'Bronze' },
      { id: 'C5', kind: 'customer', name: 'Tailwind', tier: 'Silver' },
    ];
    let a = 20260906 >>> 0;
    const rnd = () => { a = (a + 0x6d2b79f5) >>> 0; let t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };
    const ORDERS = Array.from({ length: 24 }, (_, i) => ({
      id: 'O-' + String(1000 + i),
      kind: 'order',
      customerId: 'C' + (1 + Math.floor(rnd() * 5)),
      amount: 200 + Math.round(rnd() * 9_800),
    }));
    el.textContent = '';
    const bar = document.createElement('div');
    bar.style.cssText = 'display:flex;gap:12px;align-items:center;margin-bottom:12px;flex-wrap:wrap';
    const loadBtn = document.createElement('button');
    loadBtn.type = 'button';
    loadBtn.textContent = 'Load the remaining customers';
    loadBtn.style.cssText = 'font:600 13px system-ui;padding:7px 13px;border:1px solid var(--rule);border-radius:8px;background:var(--paper);color:var(--ink-1);cursor:pointer';
    const note = document.createElement('span');
    note.style.cssText = 'font:12.5px system-ui;color:var(--ink-3)';
    bar.append(loadBtn, note);
    const gridsEl = document.createElement('div');
    gridsEl.style.cssText = 'display:grid;grid-template-columns:1.4fr 1fr;gap:12px';
    const ordersCol = document.createElement('div'); ordersCol.style.minWidth = '0';
    const custCol = document.createElement('div'); custCol.style.minWidth = '0';
    const ordersEl = document.createElement('div');
    ordersEl.style.cssText = 'height:360px;border:1px solid var(--rule);border-radius:9px;background:var(--paper);min-width:0';
    const custEl = document.createElement('div');
    custEl.style.cssText = 'height:360px;border:1px solid var(--rule);border-radius:9px;background:var(--paper);min-width:0';
    ordersCol.append(gridTitle('Orders, enriched with the customer'), ordersEl);
    custCol.append(gridTitle('Customers (the lookup)'), custEl);
    gridsEl.append(ordersCol, custCol);
    el.append(bar, gridsEl);
    const ordersGrid = LG.createGrid(ordersEl, {
      rowKey: 'id', theme: 'light',
      columns: [
        { field: 'id', title: 'Order', layout: { width: 110, pin: 'start' } },
        { field: 'customerId', title: 'Cust. id', layout: { width: 100 } },
        { field: 'name', title: 'Customer' },
        { field: 'tier', title: 'Tier', cell: { decoration: 'pill', variant: TIER_VARIANTS }, layout: { width: 110 } },
        { field: 'amount', title: 'Amount', type: 'number', format: money, total: 'sum' },
      ],
      rows: [],
    });
    const custGrid = LG.createGrid(custEl, {
      rowKey: 'id', theme: 'light', edit: true,
      columns: [
        { field: 'id', title: 'Id', layout: { width: 90, pin: 'start' } },
        { field: 'name', title: 'Customer' },
        { field: 'tier', title: 'Tier', edit: true, cell: { decoration: 'pill', variant: TIER_VARIANTS }, layout: { width: 120 } },
      ],
      rows: [],
    });
    let router: any;
    let ordersSrc: any;
    let customersSrc: any;
    let loaded = false;
    const refresh = () => {
      const shown = ordersGrid.rows.count();
      const held = ORDERS.length - shown;
      note.textContent = held
        ? `${shown} orders enriched, ${held} held until their customer is known`
        : `all ${shown} orders enriched`;
    };
    loadDataRouter()
      .then((dr: any) => {
        router = dr.createDataRouter({ key: 'kind', rowKey: 'id' });
        router.attach(ordersGrid, 'order');
        router.attach(custGrid, 'customer');
        ordersSrc = router.addSource('orders', {
          join: { from: 'customers', localKey: 'customerId', foreignKey: 'id', fields: ['name', 'tier'], missing: 'hold' },
        });
        customersSrc = router.addSource('customers');
        customersSrc.load(CUSTOMERS.slice(0, 2));
        ordersSrc.load(ORDERS);
        refresh();
      })
      .catch((err: any) => { note.textContent = 'could not load the data router: ' + (err?.message ?? 'error'); console.error('[data-router-join]', err); });
    loadBtn.addEventListener('click', () => {
      if (loaded || !customersSrc) return;
      loaded = true;
      loadBtn.disabled = true;
      customersSrc.load(CUSTOMERS);
      setTimeout(refresh, 0);
    });
    return () => {
      router?.destroy?.();
      ordersGrid?.destroy?.();
      custGrid?.destroy?.();
    };
  },
}),

One feed, enriched from a lookup as it arrives

Live records rarely arrive complete. An order comes in carrying a customer id and an amount, and the name and tier a reader actually wants live in a separate customer list. The data router joins the two as the feed arrives: an order source names the customer source as its lookup, brings the chosen fields across, and hands the grid a row that already carries the name and tier. You configure the join once, addSource('orders', { join: { from: 'customers', localKey: 'customerId', fields: ['name', 'tier'] } }), and every order from then on is enriched on the way in.

The interesting case is the order that arrives before its customer is known. Setting missing: 'hold' withholds that order rather than showing it half-filled with blank columns; when the customer feed loads, the held orders release and arrive already enriched. Both sides stay live, so a corrected tier in the customer list re-enriches the orders that point at it, and the grid reading the orders never has to know the join is happening.

How do I enrich a live feed with data from another source?

Add the lookup as a second source and give the main source a join: name the lookup in from, the field on your rows that matches it in localKey, and the fields to bring across in fields. Rows are enriched as they arrive. Set missing: 'hold' to withhold a row whose lookup is not known yet and release it once the lookup loads, so a reader never sees a half-filled record. Both sources stay live, so editing the lookup re-enriches the rows that depend on it.