# Getting Started

Free Javascript Dashboard / Record List Component

## DashboardJS

\
[DashboardJS](https://www.dashboardjs.net) ([www.dashboardjs.net](http://www.dashboardjs.net)) is a free, modular, responsive, open source Dashboard / Record List component to display records in a sleek and modern way, built entirely in vanilla Js, with zero dependancies. You can have different tabs that show different recordsets complete with pagination, sorting, filtering, and you can switch views for each recordset between Card view, and List view, and each record, field and action come with a multitude of events that you can hook onto to process data and change behaviour.

<figure><img src="https://435553087-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FO2dIT0kkascj8KGfwaXf%2Fuploads%2FYP3mm38q922fAgVuWtfs%2Fdashboardjs.png?alt=media&amp;token=0fb64725-78d2-42ed-9f3b-e07607b4e8d0" alt=""><figcaption><p>DashboardJS in Card View Mode</p></figcaption></figure>

DashboardJS is fully themeable, all you need is knowledge of HTML & CSS.

DashboardJS works either Synchronously (full data loaded and fed into the Dashboard component before initiation), or Asnychronously (Dashboard loads page by page through Fetch API).

Download: [DashboardJS.zip](https://github.com/hishamfangs/DashboardJS/tree/main/dist/DashboardJS.zip)\
Website:[ https://www.dashboardjs.net](https://www.dashboardjs.net)

### Setting up

This is the basic HTML to load the dashboard

```html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
	<head>
		<title>My Dashboard</title>
		<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
		<link rel="stylesheet" href="./dashboardjs/css/rules.css">		
		<link rel="stylesheet" href="./dashboardjs/css/theme.css">		
		<script src="./dashboardjs/js/dashboard-all.js"></script>	
		<script>	
		</script> 
	</head>
	<body>	
		<div class="dashboard-container">
			
		</div>
		<script src="./dashboardjs/js/load-dashboard.js"></script>	
	</body>	
</html>
```

### Simplest Example:

```
load-dashboard.js:
```

```javascript
var dashboard = new FutureLabs.Dashboard({
  data: [{
      "Name": "Jessie Bambergans",
      "Status": "Married",
      "Date": "1980-08-10",
      "Gender": "Female"
    },{
      "Name": "Jerome Berner",
      "Status": "Single",
      "Date": "1980-08-10",
      "Gender": "Male"
    },{
      "Name": "Ruba Jackman",
      "Status": "Married",
      "Date": "1984-01-05",
      "Gender": "Female"
  }],
  templateURL: './dashboardjs/dashboard.html',
  appendTo: document.querySelector(".dashboard-container")
});
```

### Adding tabs, fields and actions

Everything past the raw data is optional. Add `config` when you want to rename a field, change how a value is displayed, or attach behaviour.

```javascript
var dashboard = new FutureLabs.Dashboard({
  config: {
    tabs: {
      'User Profiles': {
        icon: 'far fa-user',
        description: 'A list of all approved users',
        viewMode: 'Cards',              // 'Cards' or 'List'
        itemsPerPage: 12,

        recordSettings: {
          image: { url: 'imageURL', height: '200px' },

          fields: {
            Date:   { name: 'Date of Birth', dataType: 'Date', width: '100px' },
            Name:   { name: 'Name', position: 'left' },
            Status: {
              name: 'Marital Status',
              position: 'right',
              // Whatever you return here is displayed. HTML is allowed.
              value: ({ value, record }) =>
                record.Gender === 'Female' && value === 'Married'
                  ? '<b style="color:#72de72">' + value + '</b>'
                  : value
            },
            Gender: {
              name: 'Gender',
              position: 'right',
              icon: ({ value }) => value === 'Female' ? 'fas fa-venus' : 'fas fa-mars'
            }
          },

          actionsType: 'menu',          // 'buttons' (default) or 'menu'
          actions: {
            'More details...': { icon: 'info-icon', onClick: ({ record }) => showDetails(record) },
            'Pay': {
              icon: 'pay-icon',
              // 'disable' greys the button out rather than removing it
              visibility: ({ record }) => record.Balance > 0 ? 'show' : 'disable',
              onClick: ({ record }) => startPayment(record.InvoiceId)
            },
            'Delete': {
              icon: 'cancel-icon',
              // Nothing is removed unless this returns something other than false
              onBeforeRemove: ({ record }) => confirm('Remove ' + record.Name + '?')
            }
          }
        }
      }
    }
  },
  data: data,
  templateURL: 'dashboardjs/dashboard.html',
  appendTo: '.dashboard-container'
});
```

### Where to go next

The config object nests the same way the UI does:

| Page                                                             | Covers                                                         |
| ---------------------------------------------------------------- | -------------------------------------------------------------- |
| [Config](/configuration-options/config)                          | Top-level options, profile, and a full two-tab example         |
| [Tabs](/configuration-options/config/tabs)                       | One entry per recordset — icon, description, view mode, paging |
| [Record Settings](/configuration-options/config/record-settings) | Images, field layout, and record-level events                  |
| [Fields](/configuration-options/config/fields)                   | Columns — naming, formatting, linking, visibility              |
| [Actions](/configuration-options/config/actions)                 | Per-record buttons and menus                                   |
| [Callbacks](/configuration-options/config/callbacks)             | How every function in the config is called                     |

Any of `value`, `icon`, `url`, `visibility`, `class`, `style` and `width` may be a function instead of a static value, and every component accepts `onClick`, `onRender`, `onMount` and `onBeforeRemove`. Every one of them receives a single context object — destructure the keys you need:

```javascript
value:   ({ value, record }) => record.Gender === 'Female' ? value.toUpperCase() : value,
onClick: ({ record, event }) => { event.stopPropagation(); open(record.Id); }
```

See [Callbacks](/configuration-options/config/callbacks) for every context key and what each hook returns.


# Migrating to 1.2

What changed in the callback contract, and what you need to do

**Existing dashboards keep working.** Every renamed hook still accepts its old name, and handlers written in the older positional or `this`-based styles still run. Each deprecated key logs one warning naming its replacement, so the console tells you what to change.

Two behaviours did change, and both are covered below.

## Renamed hooks

| Old          | New              |
| ------------ | ---------------- |
| `onGetValue` | `value`          |
| `onLoop`     | `onRender`       |
| `onAdd`      | `onMount`        |
| `onRemove`   | `onBeforeRemove` |

```javascript
// Before
Status: { onGetValue: function (field) { return field.data.toUpperCase(); } }

// After
Status: { value: ({ value }) => value.toUpperCase() }
```

## One context object

Every callback now receives a single context object. See [Callbacks](/configuration-options/config/callbacks).

```javascript
Status: {
  value:      ({ value, record }) => record.Gender === 'Female' ? value.toUpperCase() : value,
  visibility: ({ record })        => record.Status ? 'show' : 'hide',
  onClick:    ({ record, event }) => { event.stopPropagation(); open(record.Id); }
}
```

| Key         | What it is                                  |
| ----------- | ------------------------------------------- |
| `value`     | The field's own value. Fields only.         |
| `record`    | The whole row. The same on every component. |
| `component` | The Field, Action or Record itself.         |
| `el`        | The rendered DOM node.                      |
| `dashboard` | The Dashboard instance.                     |
| `event`     | The DOM event. `onClick` only.              |

## Breaking: a field's onClick

A field's second argument used to be the field's **value**, despite being named `record` in the old examples. It is now the row, on every component.

```javascript
// Before - `record` was actually the string "Jessie Bambergans"
onClick: function (field, record) { console.log(record); }

// After
onClick: ({ value, record }) => console.log(value, record)
```

If you relied on that argument being the value, read `value` from the context.

## Breaking: onRemove

`onRemove` received a `DashboardEvent` and you completed it to allow the removal. `onBeforeRemove` uses the return value instead.

```javascript
// Before
onRemove: function (event) {
  if (confirm('Remove?')) event.triggerCompleted();
}

// After
onBeforeRemove: ({ record }) => confirm('Remove ' + record.Name + '?')
```

Return `false` to cancel, a promise to defer, anything else to proceed:

```javascript
onBeforeRemove: async ({ record }) => {
  const response = await fetch('/api/people/' + record.Id, { method: 'DELETE' });
  return response.ok;
}
```

Configs still using `onRemove` keep the old event behaviour, so nothing breaks until you rename it.

## Fixed behaviours you may have worked around

<details>

<summary><code>value</code> is no longer ignored on Date fields</summary>

The date template used to be built after the callback ran and overwrote whatever it returned, so `onGetValue` silently did nothing on a `dataType: 'Date'` field. Your hook now wins; the date template is the default when it returns `undefined`.

If you avoided `dataType: 'Date'` because of this, you can use both together now.

</details>

<details>

<summary>A falsy return is honoured</summary>

The old assignment was guarded by `if (processedValue)`, so returning `''` left the original value on screen and a field could not be blanked. Only `undefined` now means "use the default".

```javascript
value: ({ value }) => value === 'N/A' ? '' : value    // now actually blanks it
```

If you returned `''` expecting nothing to happen, return `undefined` instead.

</details>

<details>

<summary><code>onRender</code> fires once per component</summary>

`onLoop` ran from two call sites per render pass. It now runs once per component.

A record is still built once for Card view and once for List view, so a field's hook fires for each — keep the body idempotent.

</details>

<details>

<summary>Conditional links no longer leave a broken href</summary>

A function-valued `url` was passed to the link builder before being resolved, so the function object was briefly written into the `href`, and a falsy result left `href="null"` behind. A falsy return now renders no anchor at all.

</details>

<details>

<summary><code>record</code> and <code>dashboard</code> are set everywhere</summary>

`record` was missing on records themselves, and `dashboard` was `null` on every child component. Both are now available on every component and in every context object.

</details>

## New in 1.2

* **`icon`, `class`, `style` and `width` may be functions**, joining `value`, `url` and `visibility`.
* **`onMount`** fires as a component is added to its parent.
* **A callback that throws is contained** — the default is used and the error is logged, instead of the render failing.

## Suggested order

1. Upgrade and load your dashboard. Every deprecated key you use logs a warning naming its replacement.
2. Rename them.
3. Check any field `onClick` that used the second argument.
4. Convert `onRemove` to `onBeforeRemove` and return a boolean instead of completing an event.
5. Move the rest to the destructured form as you touch them — the old styles keep working.


# Data

DashboardJS takes its records either **synchronously** — you hand it everything up front — or **asynchronously**, fetching one page at a time from an API.

## Synchronous

Pass an array and you get a single tab:

```javascript
var dashboard = new FutureLabs.Dashboard({
  data: [
    { Name: 'Jessie Bambergans', Status: 'Married', Date: '1980-08-10', Gender: 'Female' },
    { Name: 'Jerome Berner',     Status: 'Single',  Date: '1980-08-10', Gender: 'Male'   }
  ]
});
```

Pass an object keyed by tab name for several:

```javascript
var dashboard = new FutureLabs.Dashboard({
  config: { tabs: { 'User Profiles': {}, 'Invoices': {} } },
  data: {
    'User Profiles': [ { Name: 'Jessie Bambergans', Status: 'Married' } ],
    'Invoices':      [ { Customer: 'Jessie Bambergans', Balance: 240 } ]
  }
});
```

The keys must match the keys in [`config.tabs`](/configuration-options/config/tabs).

## The shape of a record

A record is a flat object. Its keys are what [`fields`](/configuration-options/config/fields) map onto:

```javascript
{ "Name": "Jessie Bambergans", "Status": "Married", "Date": "1980-08-10" }
```

Nothing is required. With no `fields` config, DashboardJS infers the columns from the first record — so the array above renders on its own.

Values are read straight from the key, so nested data needs flattening first, or a [`value`](/configuration-options/config/callbacks) hook:

```javascript
Country: { value: ({ record }) => record.address && record.address.country }
```

## Images

A record image is a **key in your data** holding the URL, not the URL itself:

```javascript
recordSettings: { image: { url: 'imageURL', height: '200px' } }
```

```javascript
{ "Name": "Jessie Bambergans", "imageURL": "/img/jessie.jpg" }
```

Records with nothing at that key fall back to a placeholder.

## Asynchronous

Give a tab a `fetch` block and it loads a page at a time:

```javascript
tabs: {
  'User Profiles': {
    itemsPerPage: 20,
    fetch: { url: '/api/people' }
  }
}
```

The server receives the page, sort, filter and tab name, and returns that slice plus a total:

```json
{ "data": [ /* one page of rows */ ], "count": 240 }
```

Full details, including renaming parameters and supplying your own loader, on [Fetch API](/configuration-options/config/fetch-api).

## Which to choose

|                       | Synchronous         | Asynchronous                |
| --------------------- | ------------------- | --------------------------- |
| Data                  | All of it, up front | One page at a time          |
| Sorting and filtering | In the browser      | On the server               |
| `count`               | Inferred            | You must return it          |
| Suits                 | Hundreds of rows    | Thousands, or a live source |

Sorting and filtering move to the server in async mode because only one page is ever in memory — the browser cannot order a set it has not seen.

## Replacing data at runtime

Each tab's [DataManager](/classes-and-apis/datamanager.js) owns its rows:

```javascript
const dm = dashboard.getChild('User Profiles').dataManager;

dm.setData(rows);           // replace the rows
dm.setData(rows, 240);      // ...and set the total, when the server pages for you
dm.refresh();               // re-run the pipeline, re-fetching if async
dm.reset();                 // clear search, filtering and sorting
```

## The pipeline

Whatever the source, rows pass through the same chain, and each stage is inspectable:

```
raw  →  searched  →  filtered  →  sorted  →  paged
```

```javascript
console.log(dm.data.raw.length, dm.data.filtered.length, dm.data.paged.length);
```


# Config

`config` describes the dashboard. It is optional — pass only `data` and you get a working dashboard with sensible defaults.

```javascript
var dashboard = new FutureLabs.Dashboard({
  language: 'en-US',
  config: { /* ... */ },
  data: { /* ... */ }
});
```

## Top-level properties

| Property           | Type   | What it does                                                                                  |
| ------------------ | ------ | --------------------------------------------------------------------------------------------- |
| `language`         | string | Active language code. Defaults to `'en-US'`. Drives every `translation` object in the config. |
| `profile`          | object | The user card in the sidebar.                                                                 |
| `initialActiveTab` | string | Which tab opens first. Defaults to the first one.                                             |
| `tabs`             | object | One entry per recordset. See [Tabs](/configuration-options/config/tabs).                      |

`language` sits **outside** `config`, alongside it — everything else lives inside.

## Profile

```javascript
profile: {
  name: 'John Addams',
  image: 'dashboardjs/assets/jadams.jpg',
  url: 'https://www.example.com',
  urlTarget: '_blank',
  translation: { 'ar-AE': 'عبد الله المستكاوي' }
}
```

## The shape

Config nests the same way the UI does — dashboard, then tabs, then the record inside a tab, then that record's fields and actions:

```
config
└── tabs
    └── 'User Profiles'          → Tabs
        ├── icon, description, viewMode, itemsPerPage, recordsGrid
        └── recordSettings        → Record Settings
            ├── image, fieldsGrid, onClick
            ├── fields            → Fields
            └── actions           → Actions
```

Each level is documented on its own page: [Tabs](/configuration-options/config/tabs), [Record Settings](/configuration-options/config/record-settings), [Fields](/configuration-options/config/fields), [Actions](/configuration-options/config/actions).

## Functions anywhere

Any of `value`, `icon`, `url`, `visibility`, `class`, `style` and `width` may be a function instead of a static value, and every component accepts `onClick`, `onRender`, `onMount` and `onBeforeRemove`. They all share one calling convention — see [Callbacks](/configuration-options/config/callbacks).

## A complete example

Two tabs, showing most of what config can do without repeating itself.

```javascript
var dashboard = new FutureLabs.Dashboard({
  language: 'en-US',
  config: {
    profile: {
      name: 'John Addams',
      image: 'dashboardjs/assets/jadams.jpg',
      translation: { 'ar-AE': 'عبد الله المستكاوي' }
    },
    initialActiveTab: 'User Profiles',
    tabs: {
      'User Profiles': {
        icon: 'far fa-user',
        description: {
          'en-US': 'A list of all approved users',
          'ar-AE': 'قائمة بجميع المستخدمين المعتمدين'
        },
        translation: { 'ar-AE': 'ملفات تعريف المستخدم' },
        viewMode: 'Cards',
        itemsPerPage: 12,
        recordsGrid: { 'grid-template-columns': '1fr 1fr 1fr', 'gap': '20px' },

        recordSettings: {
          image: { url: 'imageURL', height: '200px' },
          fieldsGrid: { 'grid-template-columns': '1fr 1fr', 'gap': '15px' },
          onClick: ({ record }) => openProfile(record.Id),

          fields: {
            Date:   { name: 'Date of Birth', dataType: 'Date', width: '100px' },
            Name:   {
              name: 'Name',
              position: 'left',
              url: ({ record }) => record.ProfileId ? '/profile/' + record.ProfileId : null,
              visibility: ({ value }) => value.includes('(disabled)') ? 'disable' : 'show'
            },
            Status: {
              name: 'Marital Status',
              position: 'right',
              value: ({ value, record }) =>
                record.Gender === 'Female' && value === 'Married'
                  ? '<b style="color:#72de72">' + value + '</b>'
                  : value
            },
            Gender: {
              name: 'Gender',
              position: 'right',
              icon: ({ value }) => value === 'Female' ? 'fas fa-venus' : 'fas fa-mars'
            }
          },

          actionsType: 'menu',
          actions: {
            'More details...': { icon: 'info-icon', onClick: ({ record }) => showDetails(record) },
            'Edit':            { icon: 'edit-icon', onClick: ({ record }) => edit(record) },
            'Delete':          {
              icon: 'cancel-icon',
              onBeforeRemove: ({ record }) => confirm('Remove ' + record.Name + '?')
            }
          }
        }
      },

      'Invoices': {
        icon: 'fas fa-file-invoice',
        description: 'Outstanding and settled invoices',
        viewMode: 'List',
        itemsPerPage: 20,

        recordSettings: {
          fields: {
            Name:    { name: 'Customer', position: 'left', style: { 'grid-column': 'span 2' } },
            Balance: {
              name: 'Balance',
              position: 'right',
              icon: 'fas fa-money-bill-wave',
              value: ({ value }) => value ? '$' + Number(value).toFixed(2) : '—'
            },
            Date:    { name: 'Issued', dataType: 'Date' }
          },
          actions: {
            'Pay': {
              icon: 'pay-icon',
              visibility: ({ record }) => record.Balance > 0 ? 'show' : 'disable',
              onClick: ({ record }) => startPayment(record.InvoiceId)
            }
          }
        }
      }
    }
  },
  data: data
});
```


# Fetch API

Point a tab at a URL and DashboardJS loads one page at a time, sending the current page, sort, filter and tab name with every request.

```javascript
tabs: {
  'User Profiles': {
    itemsPerPage: 20,
    fetch: {
      url: '/api/people',
      options: { method: 'GET' }
    }
  }
}
```

## Config

| Property              | Type   | What it does                                                                                                   |
| --------------------- | ------ | -------------------------------------------------------------------------------------------------------------- |
| `url`                 | string | The endpoint.                                                                                                  |
| `options`             | object | Passed straight to `fetch()` — `method`, `headers`, `credentials`, and so on. Defaults to `{ method: 'GET' }`. |
| `dashboardParameters` | object | Renames the parameters DashboardJS sends.                                                                      |

## What gets sent

Every request carries six parameters:

| Parameter      | Value                                                                                              |
| -------------- | -------------------------------------------------------------------------------------------------- |
| `page`         | Current page, 1-based.                                                                             |
| `itemsPerPage` | Records per page.                                                                                  |
| `getCount`     | `true` when asking only for a total, otherwise `false`.                                            |
| `filterBy`     | The active keywords, JSON-encoded: `["Married","Female"]`.                                         |
| `sortBy`       | The sort state, JSON-encoded: `{"sortBy":"Date","sortDirection":"desc","sortFieldText":"Issued"}`. |
| `tabName`      | Which tab is asking.                                                                               |

On `GET` they are appended as a query string. On `POST` they are sent as the body, as `URLSearchParams`.

```
GET /api/people?page=2&itemsPerPage=20&getCount=false&filterBy=%5B%5D&sortBy=%7B%7D&tabName=User+Profiles
```

### Renaming them

If your API expects different names, map them:

```javascript
fetch: {
  url: '/api/people',
  dashboardParameters: {
    page: 'pageNumber',
    itemsPerPage: 'limit',
    sortBy: 'order',
    filterBy: 'q'
  }
}
```

Only the keys you list are renamed; the rest keep their defaults.

## What to return

```json
{
  "data": [
    { "Name": "Jessie Bambergans", "Status": "Married" },
    { "Name": "Jerome Berner", "Status": "Single" }
  ],
  "count": 240
}
```

| Key     | Required       | What it is                                                                                                       |
| ------- | -------------- | ---------------------------------------------------------------------------------------------------------------- |
| `data`  | yes            | One page of rows.                                                                                                |
| `count` | yes for paging | The **total** across all pages, not the length of `data`. Without it the pager cannot know how many pages exist. |

{% hint style="info" %}
A bare array is also accepted and counted as the total — fine for a single-page endpoint, but it cannot support paging, because every response would claim the total equals the page size.
{% endhint %}

A response that is neither of those is reported by name rather than failing silently. That failure used to be badly disguised: reading `.data` off `undefined` threw, the throw was swallowed, and the tab rendered empty with a count of 0 — indistinguishable from a query that legitimately matched nothing.

## Sorting and filtering move to the server

When a tab fetches, only one page is ever in memory, so the browser cannot sort or filter the full set. Both are sent with the request and the server is expected to honour them. See [Sorting](/dashboard-tools/sorting) and [Filtering](/dashboard-tools/filtering).

## Headers and authentication

`options` is passed to `fetch()` untouched:

```javascript
fetch: {
  url: '/api/people',
  options: {
    method: 'POST',
    credentials: 'include',
    headers: { 'Authorization': 'Bearer ' + token }
  }
}
```

## fetchFunction

When the request does not fit a plain `fetch` — a GraphQL client, an SDK, a signed request — supply a function instead. It receives the same parameters and returns the same shape.

```javascript
tabs: {
  'User Profiles': {
    fetchFunction: async (params) => {
      const result = await api.people.list({
        page: params.page,
        perPage: params.itemsPerPage,
        sort: JSON.parse(params.sortBy || '{}')
      });
      return { data: result.rows, count: result.total };
    }
  }
}
```

`fetchFunction` takes precedence over `fetch.url`.

## One request at a time

Fetches are chained so only one is ever in flight, and a refresh arriving mid-flight is queued rather than racing the active one. Typing quickly in the keyword box or clicking through pages settles on the last request instead of whichever response happens to land last.

## Driving it yourself

```javascript
const dm = dashboard.getChild('User Profiles').dataManager;

dm.refresh();          // re-fetch the current page
dm.goToPage(2);        // fetch page 2
dm.load(true);         // count only — updates the badge, leaves rows alone
```

See [DataManager.js](/classes-and-apis/datamanager.js).


# Tabs

Each key in `tabs` becomes a tab, and the key is the tab's name. A tab holds one recordset plus the tools around it.

```javascript
config: {
  tabs: {
    'User Profiles': { /* ... */ },
    'Invoices':      { /* ... */ }
  }
}
```

Your `data` is keyed by the same names. See [Data](/configuration-options/data).

## Properties

| Property         | Type                  | What it does                                                                                                 |
| ---------------- | --------------------- | ------------------------------------------------------------------------------------------------------------ |
| `icon`           | string                | Class name for the tab icon. FontAwesome classes work, or your own from `theme.css`.                         |
| `description`    | string \| object      | Blurb shown above the records. Pass an object keyed by language to translate it.                             |
| `translation`    | object                | Language-code map for the tab's own name.                                                                    |
| `viewMode`       | `'Cards'` \| `'List'` | The tab's default view. See [View Modes](/dashboard-tools/view-modes).                                       |
| `itemsPerPage`   | number                | Records per page. Defaults to `12`. See [Pagination](/dashboard-tools/pagination).                           |
| `page`           | number                | The page to open on. 1-based.                                                                                |
| `recordsGrid`    | object                | CSS Grid pairs laying out the records in Card view.                                                          |
| `sorting`        | object                | Default sort. See [Sorting](/dashboard-tools/sorting).                                                       |
| `fetch`          | object                | Load this tab's data from a server. See [Fetch API](/configuration-options/config/fetch-api).                |
| `fetchFunction`  | function              | Supply your own loader instead of `fetch`.                                                                   |
| `recordSettings` | object                | Everything about a record in this tab. See [Record Settings](/configuration-options/config/record-settings). |

## Which tab opens first

`initialActiveTab` names it; otherwise the first entry wins.

```javascript
config: {
  initialActiveTab: 'Invoices',
  tabs: { 'User Profiles': { /* ... */ }, 'Invoices': { /* ... */ } }
}
```

## Card layout

`recordsGrid` takes raw CSS Grid property/value pairs, so the arrangement is entirely yours:

```javascript
recordsGrid: {
  'grid-template-columns': '1fr 1fr 1fr',
  'gap': '20px',
  'justify-items': 'stretch'
}
```

That lays out the records. Field layout *inside* a record is `fieldsGrid` on [Record Settings](/configuration-options/config/record-settings).

## Translating a tab

`translation` covers the tab name; `description` takes a language map of its own.

```javascript
'User Profiles': {
  translation: { 'ar-AE': 'ملفات تعريف المستخدم' },
  description: {
    'en-US': 'A list of all approved users',
    'ar-AE': 'قائمة بجميع المستخدمين المعتمدين'
  }
}
```

See [Internationalization & Localization](/configuration-options/config/internationalization-and-localization).

## A tab per source

Tabs are independent — one can hold data you passed in, another can fetch from an API:

```javascript
tabs: {
  'User Profiles': {
    viewMode: 'Cards',
    recordSettings: { fields: { Name: { name: 'Name' } } }
  },
  'Invoices': {
    viewMode: 'List',
    itemsPerPage: 20,
    fetch: { url: '/api/invoices' },
    sorting: { sortBy: 'Date', sortDirection: 'desc' },
    recordSettings: { fields: { Balance: { name: 'Balance', position: 'right' } } }
  }
}
```

## Reaching a tab at runtime

```javascript
const tab = dashboard.getChild('Invoices');
tab.setActive(true);
tab.setView('Cards');
tab.refresh();
```

See [Tab.js](/classes-and-apis/tab.js).


# Record Settings

`recordSettings` describes a single record inside a tab — its image, its layout, its fields, its actions, and the events for the record as a whole.

```javascript
tabs: {
  'User Profiles': {
    recordSettings: {
      fields:  { Name: { name: 'Name' } },
      actions: { 'Edit': { icon: 'edit-icon', onClick: ({ record }) => edit(record) } }
    }
  }
}
```

## Properties

| Property         | Type                    | What it does                                                                |
| ---------------- | ----------------------- | --------------------------------------------------------------------------- |
| `fields`         | object                  | The record's columns. See [Fields](/configuration-options/config/fields).   |
| `actions`        | object                  | The record's buttons. See [Actions](/configuration-options/config/actions). |
| `actionsType`    | `'buttons'` \| `'menu'` | How actions are presented. Defaults to `buttons`.                           |
| `image`          | object                  | Renders an image on the record. See below.                                  |
| `fieldsGrid`     | object                  | CSS Grid property/value pairs laying out the fields inside a record.        |
| `class`          | string                  | Extra CSS class on the record.                                              |
| `style`          | object                  | CSS property/value pairs applied to the record.                             |
| `onClick`        | function                | Fires when anywhere on the record is clicked.                               |
| `onRender`       | function                | Fires as the record renders.                                                |
| `onMount`        | function                | Fires as the record is added to the recordset.                              |
| `onBeforeRemove` | function                | Guards removal of the record.                                               |

## Images

`image.url` is the **key in your data** that holds the image URL, not the URL itself.

```javascript
image: {
  url: 'imageURL',      // data key, e.g. record.imageURL
  height: '200px'       // height in Card view
}
```

Records with no value at that key fall back to a placeholder image.

## Laying out fields

`fieldsGrid` takes raw CSS Grid pairs, so the arrangement of fields within a card is entirely yours.

```javascript
fieldsGrid: {
  'grid-template-columns': '1fr 1fr',
  'gap': '15px',
  'justify-items': 'stretch'
}
```

Individual fields can span columns with their own `style: { 'grid-column': 'span 2' }`.

## Record events

A record's context carries its row as `record`. See [Callbacks](/configuration-options/config/callbacks) for the full list of context keys.

```javascript
recordSettings: {
  onClick: ({ record }) => openDetails(record.Id),

  // Nothing is removed until this resolves to something other than false.
  onBeforeRemove: ({ record }) => confirm('Remove ' + record.Name + '?')
}
```

{% hint style="info" %}
A field's `onClick` fires before the record's. Call `event.stopPropagation()` in the field handler if you don't want both to run.
{% endhint %}

## A worked example

```javascript
recordSettings: {
  image: { url: 'imageURL', height: '200px' },

  fieldsGrid: {
    'grid-template-columns': '1fr 1fr',
    'gap': '15px'
  },

  onClick: ({ record }) => openDetails(record.Id),

  fields: {
    Date:   { name: 'Date of Birth', dataType: 'Date', width: '100px' },
    Name:   { name: 'Name', position: 'left' },
    Status: {
      name: 'Marital Status',
      position: 'right',
      value: ({ value, record }) =>
        record.Gender === 'Female' && value === 'Married'
          ? '<b style="color:#72de72">' + value + '</b>'
          : value
    },
    Description: {
      position: 'left',
      class: 'justify',
      style: { 'grid-column': 'span 2' },
      value: ({ value }) => value || '<span style="color:#c3c3c3">N/A</span>'
    }
  },

  actionsType: 'menu',
  actions: {
    'Edit':   { icon: 'edit-icon',   onClick: ({ record }) => edit(record) },
    'Pay':    { icon: 'pay-icon',    visibility: ({ record }) => record.Balance > 0 ? 'show' : 'disable' },
    'Delete': { icon: 'cancel-icon', onBeforeRemove: ({ record }) => confirm('Remove ' + record.Name + '?') }
  }
}
```


# Fields

`fields` describes the columns of a record. Each key maps to a key in your data, and its value is an object describing how that column looks and behaves.

```javascript
fields: {
  Name:   { name: 'Full Name' },
  Status: { name: 'Marital Status', position: 'right' },
  Date:   { name: 'Date of Birth', dataType: 'Date' }
}
```

Omit the object entirely and the field is rendered with its data key as the label.

## Properties

| Property      | Type                                | What it does                                                                                                                              |
| ------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `name`        | string                              | The label shown for the field. Defaults to the data key.                                                                                  |
| `position`    | `'left'` \| `'right'` \| `'center'` | Text alignment inside the card. Defaults to `center`.                                                                                     |
| `dataType`    | `'Date'`                            | Renders the value as a graphical day / month / year block. Only `'Date'` changes anything; other values are accepted and ignored.         |
| `width`       | string                              | Column width in List view.                                                                                                                |
| `class`       | string                              | Extra CSS class on the field.                                                                                                             |
| `style`       | object                              | CSS property/value pairs applied to the field, e.g. `{ 'grid-column': 'span 2' }`.                                                        |
| `icon`        | string                              | A CSS or FontAwesome class name shown beside the value.                                                                                   |
| `url`         | string                              | Wraps the value in a link. Pair with `urlTarget: '_blank'`.                                                                               |
| `translation` | object                              | Language-code map for the field's label. See [Internationalization](/configuration-options/config/internationalization-and-localization). |
| `value`       | function                            | Computes the displayed value.                                                                                                             |
| `visibility`  | function                            | Shows, hides or disables the field.                                                                                                       |
| `onClick`     | function                            | Fires when the field is clicked.                                                                                                          |
| `onRender`    | function                            | Fires as the field renders.                                                                                                               |

`value`, `icon`, `url`, `visibility`, `class`, `style` and `width` may each be given as a **function** instead of a static value. Each receives a single context object — see [Callbacks](/configuration-options/config/callbacks) for every key it carries.

## Formatting a value

Whatever `value` returns is displayed. HTML is allowed.

```javascript
Balance: {
  name: 'Balance',
  position: 'right',
  value: ({ value }) => value ? '$' + Number(value).toFixed(2) : '—'
}
```

Return `undefined` to fall through to the default rendering. Return `''` to blank the field.

```javascript
Description: {
  // Show a muted placeholder rather than an empty cell
  value: ({ value }) => value || '<span style="color:#c3c3c3">N/A</span>'
}
```

{% hint style="info" %}
`value` also wins over `dataType: 'Date'`. If you set both, your value is displayed and the date template is skipped.
{% endhint %}

## Showing, hiding and disabling

```javascript
Name: {
  // 'disable' greys the field out and removes its click handlers.
  visibility: ({ value }) => value.includes('(disabled)') ? 'disable' : 'show'
}
```

`'hide'` removes the field, `'show'` and `'enable'` display it, and `false` or `0` are accepted as shorthand for hide.

## Linking

`url` may be a string or a function. A function returning a falsy value renders no anchor at all, which is the clean way to make a link conditional.

```javascript
Name: {
  url: ({ record }) => record.ProfileId ? '/profile/' + record.ProfileId : null,
  urlTarget: '_blank'
}
```

## Reacting to a click

The field's value, its row and the DOM event all arrive on the context.

```javascript
Name: {
  onClick: ({ value, record, event }) => {
    event.stopPropagation();          // don't also fire the record's onClick
    console.log(value, 'on', record);
  }
}
```

## Layout

`style` takes raw CSS property/value pairs, so a field can span columns in the card grid.

```javascript
Description: {
  position: 'left',
  width: '400px',                      // List view column width
  class: 'justify',
  style: { 'grid-column': 'span 2' }   // Card view span
}
```

## A worked example

```javascript
fields: {
  Date: {
    name: 'Date of Birth',
    dataType: 'Date',
    width: '100px',
    translation: { 'ar-AE': 'تاريخ الميلاد' }
  },
  Name: {
    name: 'Name',
    position: 'left',
    url: ({ record }) => record.ProfileId ? '/profile/' + record.ProfileId : null,
    visibility: ({ value }) => value.includes('(disabled)') ? 'disable' : 'show',
    translation: { 'ar-AE': 'الإسم' }
  },
  Status: {
    name: 'Marital Status',
    position: 'right',
    value: ({ value, record }) =>
      record.Gender === 'Female' && value === 'Married'
        ? '<b style="color:#72de72">' + value + '</b>'
        : value,
    translation: { 'ar-AE': 'الحالة الزوجية' }
  },
  Gender: {
    name: 'Gender',
    position: 'right',
    icon: ({ value }) => value === 'Female' ? 'fas fa-venus' : 'fas fa-mars',
    translation: { 'ar-AE': 'الجنس' }
  }
}
```


# Actions

`actions` adds buttons to every record. Each key is the button's label; its value describes the icon and behaviour.

```javascript
recordSettings: {
  actionsType: 'buttons',        // 'buttons' (default) or 'menu'
  actions: {
    'Edit':   { icon: 'edit-icon',   onClick: ({ record }) => edit(record) },
    'Cancel': { icon: 'cancel-icon', onClick: ({ record }) => cancel(record) }
  }
}
```

Set `actionsType: 'menu'` to collapse them into a dropdown instead of a row of buttons.

## Properties

| Property         | Type     | What it does                                         |
| ---------------- | -------- | ---------------------------------------------------- |
| `icon`           | string   | A CSS or FontAwesome class name for the button icon. |
| `translation`    | object   | Language-code map for the action's label.            |
| `width`          | number   | Button width. Defaults to `60`.                      |
| `visibility`     | function | Shows, hides or disables the action per row.         |
| `onClick`        | function | Fires when the action is clicked.                    |
| `onBeforeRemove` | function | Guards removal of the record.                        |
| `onRender`       | function | Fires as the action renders.                         |

`icon`, `visibility`, `url`, `class`, `style` and `width` may each be a function. See [Callbacks](/configuration-options/config/callbacks).

## Reading the row

An action's context carries the row it belongs to as `record`.

```javascript
actions: {
  'Pay': {
    icon: 'pay-icon',
    onClick: ({ record }) => startPayment(record.InvoiceId)
  },
  'Email': {
    icon: 'mail-icon',
    onClick: ({ record }) => window.open('mailto:' + record.Email)
  }
}
```

## Enabling per row

Returning `'disable'` greys the button out and removes its click handler — usually better feedback than making the button vanish.

```javascript
'Pay': {
  icon: 'pay-icon',
  visibility: ({ record }) => record.Balance > 0 ? 'show' : 'disable'
}
```

Return `'hide'` to remove it entirely, `'show'` to display it.

## Per-row icons

```javascript
'Flag': {
  icon: ({ record }) => record.Flagged ? 'fas fa-flag' : 'far fa-flag'
}
```

## Confirming a removal

`onBeforeRemove` holds the removal open. Return `false` to cancel it, or a promise to defer it — nothing is removed until it resolves to something other than `false`.

```javascript
'Delete': {
  icon: 'cancel-icon',
  onBeforeRemove: ({ record }) => confirm('Remove ' + record.Name + '?')
}
```

```javascript
'Delete': {
  icon: 'cancel-icon',
  onBeforeRemove: async ({ record }) => {
    const response = await fetch('/api/people/' + record.Id, { method: 'DELETE' });
    return response.ok;                    // false leaves the row in place
  }
}
```

## A worked example

```javascript
recordSettings: {
  actionsType: 'menu',
  actions: {
    'More details...': {
      icon: 'info-icon',
      translation: { 'ar-AE': 'معلومات أخرى' },
      onClick: ({ record }) => showDetails(record)
    },
    'Pay': {
      icon: 'pay-icon',
      translation: { 'ar-AE': 'دفع' },
      visibility: ({ record }) => record.Balance > 0 ? 'show' : 'disable',
      onClick: ({ record }) => startPayment(record.InvoiceId)
    },
    'Edit': {
      icon: 'edit-icon',
      translation: { 'ar-AE': 'تعديل' },
      onClick: ({ record }) => edit(record)
    },
    'Delete': {
      icon: 'cancel-icon',
      translation: { 'ar-AE': 'حذف' },
      onBeforeRemove: ({ record }) => confirm('Remove ' + record.Name + '?')
    }
  }
}
```


# Callbacks

How every function in the config object is called

Every key in the config object is either a **noun** or a **verb**.

* **Nouns** — `value`, `icon`, `url`, `visibility`, `class`, `style`, `width` — describe what something *is*. Each may be a plain value **or** a function that returns one.
* **Verbs** — `onClick`, `onRender`, `onMount`, `onBeforeRemove` — are things that *happen*.

Both are called the same way, so there is only one convention to learn.

## One context object

Every callback receives a single context object. Destructure the keys you need:

```javascript
Status: {
  value:      ({ value, record }) => record.Gender === 'Female' ? value.toUpperCase() : value,
  visibility: ({ record })        => record.Status ? 'show' : 'hide',
  onClick:    ({ value, record }) => console.log(value, record)
}
```

| Key         | What it is                                                          |
| ----------- | ------------------------------------------------------------------- |
| `value`     | The field's own value. Fields only.                                 |
| `record`    | The whole row, as a plain data object. The same on every component. |
| `component` | The Field, Action or Record itself.                                 |
| `el`        | The rendered DOM node.                                              |
| `dashboard` | The Dashboard instance.                                             |
| `event`     | The DOM event. `onClick` only.                                      |

`value` and `record` are deliberately separate names. `data` used to mean the field's value on a Field but the whole row on an Action, and that one overload caused most of the confusion this contract replaces.

## What each one returns

| Hook             | Returns                                                                                                                                                    |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `value`          | The value to display. `undefined` means "no opinion" and the default is used. Anything else is used verbatim — **including `''`**, which blanks the field. |
| `icon`           | A CSS or FontAwesome class name.                                                                                                                           |
| `url`            | The href. A falsy return renders no `<a>` at all.                                                                                                          |
| `visibility`     | `'show'`, `'enable'`, `'disable'` or `'hide'`. `false` and `0` also mean hide.                                                                             |
| `onClick`        | Ignored.                                                                                                                                                   |
| `onRender`       | Ignored. Fires once per component as it renders.                                                                                                           |
| `onMount`        | Ignored. Fires as the component is added to its parent.                                                                                                    |
| `onBeforeRemove` | `false` cancels the removal, a promise defers it, anything else lets it proceed.                                                                           |

## Examples

**Highlight a value based on another column**

```javascript
Status: {
  value: ({ value, record }) =>
    record.Gender === 'Female' && value === 'Married'
      ? '<b style="color:#72de72">' + value + '</b>'
      : value
}
```

**Blank a field** — returning `''` works; returning `undefined` would fall through to the raw value.

```javascript
Notes: { value: ({ value }) => value === 'N/A' ? '' : value }
```

**Format a number**

```javascript
Balance: { value: ({ value }) => value ? '$' + Number(value).toFixed(2) : '—' }
```

**Pick an icon per row**

```javascript
Gender: { icon: ({ value }) => value === 'Female' ? 'fas fa-venus' : 'fas fa-mars' }
```

**Make a link conditional** — a falsy return means no anchor is rendered at all.

```javascript
Name: {
  url: ({ record }) => record.ProfileId ? '/profile/' + record.ProfileId : null,
  urlTarget: '_blank'
}
```

**Grey out an action instead of hiding it**

```javascript
Pay: { visibility: ({ record }) => record.Balance > 0 ? 'show' : 'disable' }
```

**Disable a field on its own value**

```javascript
Name: { visibility: ({ value }) => value.includes('(disabled)') ? 'disable' : 'show' }
```

**Stop a field click from also triggering the record click**

```javascript
Name: { onClick: ({ event }) => event.stopPropagation() }
```

**Confirm before removing** — nothing is removed unless this resolves to something other than `false`.

```javascript
Delete: { onBeforeRemove: ({ record }) => confirm('Remove ' + record.Name + '?') }
```

**Defer removal on a server call**

```javascript
Delete: {
  onBeforeRemove: async ({ record }) => {
    const response = await fetch('/api/people/' + record.Id, { method: 'DELETE' });
    return response.ok;                 // false leaves the row on screen
  }
}
```

**Tag a node as it renders** — `onRender` may run more than once for the same row, because a record is built for both Card and List view. Keep it idempotent.

```javascript
Status: {
  onRender: ({ record, el }) => el.classList.toggle('is-overdue', record.Balance > 0)
}
```

**Reach the dashboard from a handler**

```javascript
Refresh: { onClick: ({ dashboard }) => dashboard.refresh() }
```

## Renamed in 1.2

The old names still work and will keep working until 2.0. Each logs a deprecation warning naming its replacement.

| Old          | New              | Note                                                          |
| ------------ | ---------------- | ------------------------------------------------------------- |
| `onGetValue` | `value`          |                                                               |
| `onLoop`     | `onRender`       | Used to fire twice per pass.                                  |
| `onAdd`      | `onMount`        | No longer receives an unused `DashboardEvent`.                |
| `onRemove`   | `onBeforeRemove` | Return `false` instead of calling `event.triggerCompleted()`. |

Two long-standing surprises were fixed at the same time:

* `value` is no longer ignored on `dataType: "Date"` fields. The date template used to overwrite whatever the callback returned.
* A falsy return is now honoured, so a field can actually be blanked. Only `undefined` means "use the default".


# Internationalization & Localization

Any label in the config can be translated by adding a `translation` object next to it, keyed by language code.

```javascript
var dashboard = new FutureLabs.Dashboard({
  language: 'ar-AE',          // the active language
  config: { /* ... */ },
  data: data
});
```

`language` sits alongside `config`, not inside it. It defaults to `'en-US'`.

## Where translations go

Wherever there is a name to show — tabs, fields, actions, the profile:

```javascript
config: {
  profile: {
    name: 'John Addams',
    translation: { 'ar-AE': 'عبد الله المستكاوي' }
  },
  tabs: {
    'User Profiles': {
      translation: { 'ar-AE': 'ملفات تعريف المستخدم' },
      description: {
        'en-US': 'A list of all approved users',
        'ar-AE': 'قائمة بجميع المستخدمين المعتمدين'
      },
      recordSettings: {
        fields: {
          Date:   { name: 'Date of Birth',   translation: { 'ar-AE': 'تاريخ الميلاد' } },
          Status: { name: 'Marital Status',  translation: { 'ar-AE': 'الحالة الزوجية' } }
        },
        actions: {
          'Edit': { icon: 'edit-icon', translation: { 'ar-AE': 'تعديل' } }
        }
      }
    }
  }
}
```

## Two shapes

`translation` maps a language code to a replacement for `name`:

```javascript
Status: { name: 'Marital Status', translation: { 'ar-AE': 'الحالة الزوجية' } }
```

`description` is a language map in its own right — there is no separate English key to override:

```javascript
description: { 'en-US': 'A list of all approved users', 'ar-AE': 'قائمة بجميع المستخدمين المعتمدين' }
```

A plain string is fine when you only need one language:

```javascript
description: 'A list of all approved users'
```

## Falling back

A missing translation for the active language falls back to `name`, so a partially translated config still renders. Nothing throws and nothing renders blank.

## Translating values, not labels

`translation` covers labels. To localise the **data** — a status, a currency, a date — use the [`value`](/configuration-options/config/callbacks) hook, which has the active language on its context:

```javascript
Status: {
  name: 'Marital Status',
  translation: { 'ar-AE': 'الحالة الزوجية' },
  value: ({ value, component }) =>
    component.language === 'ar-AE'
      ? { Married: 'متزوج', Single: 'أعزب' }[value] || value
      : value
}
```

```javascript
Balance: {
  value: ({ value, component }) =>
    new Intl.NumberFormat(component.language, { style: 'currency', currency: 'AED' }).format(value)
}
```

## Right-to-left

Set `dir` on the page and the layout follows, since the grid is built with logical CSS:

```html
<html lang="ar" dir="rtl">
```

## Switching language

`language` is read when the dashboard is built. To change it afterwards, construct a new dashboard with the new code:

```javascript
function render(language) {
  document.querySelector('.dashboard-container').innerHTML = '';
  return new FutureLabs.Dashboard({
    language: language,
    config: config,
    data: data,
    templateURL: 'dashboardjs/dashboard.html',
    appendTo: '.dashboard-container'
  });
}
```


# templateURL

By default DashboardJS uses the markup already on the page. `templateURL` loads that markup from a separate file instead, which keeps your HTML page down to a single container element.

```javascript
var dashboard = new FutureLabs.Dashboard({
  data: data,
  templateURL: 'dashboardjs/dashboard.html',
  appendTo: '.dashboard-container'
});
```

<details>

<summary><code>templateURL</code> <mark style="color:blue;">string</mark></summary>

URL path to the dashboard HTML template. Relative paths resolve against the page, not the script.

</details>

## The page

Everything the dashboard needs is fetched, so the host page stays minimal:

```html
<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" href="dashboardjs/css/rules.css">
    <link rel="stylesheet" href="dashboardjs/css/theme.css">
    <script src="dashboardjs/js/dashboard-all.js"></script>
  </head>
  <body>
    <div class="dashboard-container"></div>
    <script src="dashboardjs/js/load-dashboard.js"></script>
  </body>
</html>
```

{% hint style="warning" %}
Fetching a template means the page must be served over `http://` or `https://`. Opening it as a `file://` URL is blocked by the browser's origin rules. Use the [Uses Current HTML file](https://github.com/hishamfangs/DashboardJS/tree/main/dist/README.md) example if you need it to work from the filesystem.
{% endhint %}

## Waiting for it

Loading is asynchronous. `loadingTemplate` resolves once the template is in place:

```javascript
var dashboard = new FutureLabs.Dashboard({
  data: data,
  templateURL: 'dashboardjs/dashboard.html',
  appendTo: '.dashboard-container'
});

await dashboard.loadingTemplate;
dashboard.switchView('List');
```

## Customising the template

`dashboard.html` is ordinary markup. Copy it, rearrange it, and point `templateURL` at your copy — the classes are what the library binds to, so keep those and change everything else freely. See [Theming](/configuration-options/theming).

## Without templateURL

Omit it and DashboardJS uses the markup already in the document, modifying it in place rather than cloning. That is the default, and it works from `file://`.


# appendTo

Where the dashboard mounts in your page.

```javascript
var dashboard = new FutureLabs.Dashboard({
  data: data,
  templateURL: 'dashboardjs/dashboard.html',
  appendTo: '.dashboard-container'
});
```

<details>

<summary><code>appendTo</code> <mark style="color:blue;">string | Element</mark></summary>

A CSS selector or a DOM element. The dashboard is appended inside it.

</details>

Both forms work:

```javascript
appendTo: '.dashboard-container'
appendTo: document.querySelector('#reports')
```

## What it changes

Passing `appendTo` switches the dashboard from modifying markup already on the page to **appending a fresh copy** into your container. That is `useExistingElement: false`, and `appendTo` sets it for you.

|            | Without `appendTo`                  | With `appendTo`                                     |
| ---------- | ----------------------------------- | --------------------------------------------------- |
| Markup     | Already in the page                 | Cloned from the template                            |
| Container  | The existing `.dashboard-component` | Whatever you name                                   |
| Pairs with | Inline markup                       | [`templateURL`](/configuration-options/templateurl) |

## More than one dashboard

Because each mount gets its own copy, two dashboards can live on one page:

```javascript
var people = new FutureLabs.Dashboard({
  data: peopleData,
  templateURL: 'dashboardjs/dashboard.html',
  appendTo: '#people'
});

var invoices = new FutureLabs.Dashboard({
  data: invoiceData,
  templateURL: 'dashboardjs/dashboard.html',
  appendTo: '#invoices'
});
```

## The container must exist first

`appendTo` runs when the dashboard is constructed, so the element has to be in the document by then — put the script after the container, or wait for `DOMContentLoaded`.

```html
<div class="dashboard-container"></div>
<script src="dashboardjs/js/load-dashboard.js"></script>
```


# Theming

Restyling DashboardJS with nothing but HTML and CSS

DashboardJS ships two stylesheets, and the split is the whole theming story:

| File            | What it does                                                                                  | Edit it?                 |
| --------------- | --------------------------------------------------------------------------------------------- | ------------------------ |
| `css/rules.css` | Structural rules the component needs to function — grid behaviour, show/hide, view switching. | No.                      |
| `css/theme.css` | Everything you can see: colours, spacing, radii, fonts, icons.                                | Yes. This is your theme. |

```html
<link rel="stylesheet" href="dashboardjs/css/rules.css">
<link rel="stylesheet" href="dashboardjs/css/theme.css">
```

Keep them in that order — `theme.css` is meant to win.

## Class names

Every component renders a predictable class, so you can style any part without touching JavaScript.

| Class                                                        | The component                                   |
| ------------------------------------------------------------ | ----------------------------------------------- |
| `.dashboard-component`                                       | The dashboard root.                             |
| `.tabs-component` · `.tab-component`                         | The tab strip and each tab.                     |
| `.recordset-component`                                       | The records container.                          |
| `.record-component`                                          | One record.                                     |
| `.record-image`                                              | A record's image.                               |
| `.fields-wrapper` · `.field-component`                       | The fields container and each field.            |
| `.field-title` · `.field-text` · `.field-icon`               | A field's label, value and icon.                |
| `.actions-wrapper` · `.action-component`                     | The actions container and each action.          |
| `.action-text` · `.action-icon` · `.action-link`             | Parts of an action.                             |
| `.actionsmenu-component`                                     | The dropdown when `actionsType: 'menu'`.        |
| `.fieldheadercontainer-component` · `.fieldheader-component` | Column headers, List view only.                 |
| `.filtering-component` · `.filteringkeyword-component`       | The keyword box and each keyword chip.          |
| `.sorting-component` · `.sortingitem-component`              | The sort control and its entries.               |
| `.paging-component` · `.pagebutton-component`                | The pager and its buttons.                      |
| `.viewswitcher-component`                                    | The Cards / List toggle.                        |
| `.userprofile-component`                                     | The sidebar profile card.                       |
| `.badge`                                                     | A tab's record count.                           |
| `.dashboard-tools`                                           | The row holding sort, filter and view controls. |
| `.breadcrumbs-wrapper`                                       | The tab description area.                       |

## State classes

Applied by [`visibility`](/configuration-options/config/callbacks) and the view switcher:

| Class              | Meaning                                          |
| ------------------ | ------------------------------------------------ |
| `.hide`            | Hidden.                                          |
| `.disable`         | Greyed out, click handlers removed.              |
| `.cards` · `.list` | The active view mode, set on the dashboard root. |
| `.active`          | The open tab.                                    |
| `.cursor-pointer`  | Something clickable.                             |

```css
.field-component.disable { opacity: .45; pointer-events: none; }
.dashboard-component.list .record-component { display: flex; }
```

## Your own classes

Any component takes a `class`, and it may be computed:

```javascript
Balance: {
  class: ({ record }) => record.Balance > 0 ? 'is-overdue' : 'is-settled'
}
```

```css
.is-overdue .field-text { color: #c0392b; font-weight: 600; }
```

For a class that depends on something outside the row, use `onRender`:

```javascript
recordSettings: {
  onRender: ({ record, el }) => el.classList.toggle('is-mine', record.OwnerId === currentUserId)
}
```

## Inline styles

`style` takes CSS property/value pairs and may also be a function:

```javascript
Description: { style: { 'grid-column': 'span 2' } }
Balance:     { style: ({ record }) => ({ color: record.Balance > 0 ? '#c0392b' : 'inherit' }) }
```

## Layout

Two grids, set with real CSS Grid pairs:

```javascript
'User Profiles': {
  recordsGrid: { 'grid-template-columns': '1fr 1fr 1fr', 'gap': '20px' },   // records
  recordSettings: {
    fieldsGrid: { 'grid-template-columns': '1fr 1fr', 'gap': '15px' }       // fields within a record
  }
}
```

Because they are plain CSS, media queries work as usual:

```css
@media (max-width: 720px) {
  .records-container { grid-template-columns: 1fr !important; }
}
```

## Icons

Any `icon` is a class name, so bring your own or use a library:

```javascript
Gender: { icon: 'fas fa-venus-mars' }                                    // FontAwesome
Status: { icon: ({ value }) => value === 'Married' ? 'ring-icon' : '' }  // your own
```

```css
.ring-icon { background: url('assets/ring.svg') no-repeat center / contain; width: 1em; height: 1em; }
```

The built-in icons — `info-icon`, `pay-icon`, `edit-icon`, `cancel-icon` — live in `assets/` and are wired up in `theme.css`.

## The markup

`dashboard.html` is ordinary HTML. Copy it, restructure it, and load your copy with [`templateURL`](/configuration-options/templateurl). The library binds to the class names in the table above, so keep those and change everything else.

## Building the CSS

`theme.css` is compiled from Sass:

```bash
npm run build      # sass + scripts + examples
npx gulp sass      # stylesheet only
```

Edit the `.scss` source rather than the compiled `theme.css`, or your next build will overwrite it.


# Sorting

Sorting is enabled by default. The control lists every field and sorts the current recordset when one is picked.

## Sorting from code

Each tab's [DataManager](/classes-and-apis/datamanager.js) owns the sort state.

```javascript
const dm = dashboard.getChild('User Profiles').dataManager;

dm.sort({ sortBy: 'Name', sortDirection: 'asc' });
dm.toggleSorting();          // flip the current direction
```

| Key             | Type   | What it is                                                     |
| --------------- | ------ | -------------------------------------------------------------- |
| `sortBy`        | string | The data key to sort on.                                       |
| `sortDirection` | string | `'asc'` or `'desc'`. Also `DataManager.SORTING.ASC` / `.DESC`. |
| `sortFieldText` | string | The label shown in the sort control.                           |

## A default sort

Sorting config is read by the DataManager when the tab is built:

```javascript
tabs: {
  'Invoices': {
    sorting: { sortBy: 'Date', sortDirection: 'desc' },
    recordSettings: { /* ... */ }
  }
}
```

## Sorting on the server

When a tab fetches its data, sorting is not applied in the browser — the current sort is sent with every request and the server is expected to return rows already ordered.

It arrives JSON-encoded under the `sortBy` parameter:

```
sortBy={"sortBy":"Date","sortDirection":"desc","sortFieldText":"Issued"}
```

Rename that parameter with `fetch.dashboardParameters`. See [Fetch API](/configuration-options/config/fetch-api).

## What the control shows

Sorting items are built from the same field configuration as the columns, but the per-field hooks are stripped first — a sort entry renders a label, not a row's data. `visibility`, `onClick`, `icon`, `url`, `value`, `onRender`, `onMount` and `onBeforeRemove` never cascade into it.

To change how a field appears in the sort list, set its [`name`](/configuration-options/config/fields) or `translation`.


# Filtering

Filtering is enabled by default. The keyword box above the records narrows the current recordset as you type.

## Filtering from code

```javascript
const dm = dashboard.getChild('User Profiles').dataManager;

dm.filter({ keywords: ['Married'] });   // replace the active keywords
dm.addKeyword('Female');                // add one more
dm.reset();                             // clear filtering, search and sorting
```

`filtering` is `{ keywords: [...] }`. Every keyword must match for a row to survive.

## Search vs filter

Two mechanisms, deliberately separate:

|           | Filtering                         | Search                            |
| --------- | --------------------------------- | --------------------------------- |
| Driven by | The keyword box                   | `doSearch()`                      |
| Shape     | `{ keywords: [] }`                | `{ parameters: [], options: {} }` |
| For       | Quick narrowing across all fields | Structured, per-field queries     |

```javascript
dm.doSearch({
  parameters: [{ field: 'Status', value: 'Married' }],
  options: { wholeWordSearch: true, enableSpecialCharacters: false }
});
```

| Option                    | Default | What it does                                                   |
| ------------------------- | ------- | -------------------------------------------------------------- |
| `wholeWordSearch`         | `false` | Match whole words only.                                        |
| `enableSpecialCharacters` | `false` | Treat special characters literally rather than stripping them. |

## The pipeline

Both feed the same chain, in this order:

```
raw  →  searched  →  filtered  →  sorted  →  paged
```

Each stage is available on `dm.data`, which is useful when debugging why a row disappeared:

```javascript
console.log(dm.data.raw.length, dm.data.filtered.length, dm.data.paged.length);
```

## Filtering on the server

When a tab fetches its data, filtering happens server-side. The active keywords are sent JSON-encoded with every request:

```
filterBy=["Married","Female"]
```

Rename that parameter with `fetch.dashboardParameters`. See [Fetch API](/configuration-options/config/fetch-api).


# View Modes

Every recordset renders as **Cards** or as a **List**. Each tab sets its own default, and the user can switch at any time from the view button in the toolbar.

## Config

```javascript
var dashboard = new FutureLabs.Dashboard({
  config: {
    tabs: {
      'User Profiles': {
        viewMode: 'Cards'      // 'Cards' or 'List'
      }
    }
  }
});
```

<details>

<summary><code>viewMode</code> <mark style="color:blue;">string</mark></summary>

* `'Cards'` — a grid of cards, laid out by `recordsGrid`. The default.
* `'List'` — one row per record, with a header row of column names.

Anything other than `'Cards'` (case-insensitive) is treated as List.

</details>

## What differs between them

|                 | Cards                        | List                               |
| --------------- | ---------------------------- | ---------------------------------- |
| Layout          | `recordsGrid` grid of cards  | One row per record                 |
| Column headers  | None                         | A header row, from the field names |
| Field width     | `fieldsGrid` within the card | The field's `width`                |
| Record image    | Shown, at `image.height`     | Hidden                             |
| Field alignment | `position`                   | `position`                         |

Two properties only take effect in one mode: `width` sizes a column in List view, and `image.height` sizes the picture in Card view.

```javascript
fields: {
  Date: {
    name: 'Date of Birth',
    width: '100px',        // List view column width
    position: 'left'       // alignment in both
  }
}
```

## Switching from code

Whole dashboard:

```javascript
dashboard.switchView('List');
dashboard.switchView('Cards');
```

A single tab:

```javascript
dashboard.getChild('Invoices').setView('List');
```

The mode is applied as a `.cards` or `.list` class on the dashboard root, which is what the stylesheet keys off:

```css
.dashboard-component.list .record-component { display: flex; }
```

## Column headers

List view adds a header row built from the same field configuration as the columns — but the per-field hooks are stripped first, because a header renders a label rather than a row's data. `visibility`, `onClick`, `icon`, `url`, `value`, `onRender`, `onMount` and `onBeforeRemove` never cascade into it.

To change a header, set the field's [`name`](/configuration-options/config/fields) or `translation`.

## Choosing a default

Cards suit records with an image or a handful of prominent values. List suits dense, comparable rows — invoices, transactions, logs — where scanning down a column matters more than the individual record.

```javascript
tabs: {
  'User Profiles': { viewMode: 'Cards', recordSettings: { image: { url: 'imageURL', height: '200px' } } },
  'Invoices':      { viewMode: 'List' }
}
```


# Pagination

Pagination is enabled by default and shows 12 records per page.

## Config

```javascript
var dashboard = new FutureLabs.Dashboard({
  config: {
    tabs: {
      'User Profiles': {
        itemsPerPage: 12,   // defaults to 12
        page: 1             // the page to open on, 1-based
      }
    }
  }
});
```

<details>

<summary><code>itemsPerPage</code> <mark style="color:blue;">number</mark></summary>

Records per page. Defaults to `12`. Set per tab.

</details>

<details>

<summary><code>page</code> <mark style="color:blue;">number</mark></summary>

The page to start on. 1-based. Defaults to `1`.

</details>

## Paging from code

```javascript
const dm = dashboard.getChild('User Profiles').dataManager;

dm.goToPage(3);
console.log(dm.page, 'of', dm.pages, '—', dm.count, 'records');
```

| Property | What it is                                            |
| -------- | ----------------------------------------------------- |
| `page`   | Current page, 1-based.                                |
| `pages`  | Total pages, derived from `count` and `itemsPerPage`. |
| `count`  | Total records. From the server when fetching.         |

## Paging on the server

When a tab fetches, only one page is ever in memory. `page` and `itemsPerPage` are sent with every request and the server returns that slice plus a total:

```json
{ "data": [ /* one page of rows */ ], "count": 240 }
```

`count` is what drives the pager — without it DashboardJS cannot know how many pages exist. See [Fetch API](/configuration-options/config/fetch-api).

### Updating the total without reloading

`setCount()` updates the total and the pager while leaving the rows on screen alone:

```javascript
dm.setCount(240);
```

This is what a count-only load uses. Passing such a response through `setData()` would blank a populated tab, because a count-only response carries an empty list.

## Rapid clicks

Only one page change runs at a time. A click arriving mid-flight is queued and runs when the active one finishes, and further clicks replace that queued one rather than stacking — so a burst of clicks settles on the last page you asked for instead of racing.


# Dashboard Overview

DashboardJS is a tree of components. Each one owns its own DOM node, its own slice of config, and its own children.

```
Dashboard                     the root, and what you construct
├── UserProfile               the sidebar profile card
├── Tabs                      the tab strip
│   └── Tab                   one per entry in config.tabs
│       ├── DataManager       rows, paging, sorting, filtering, fetching
│       ├── Sorting           the sort control
│       ├── Filtering         the keyword box
│       ├── ViewSwitcher      Cards / List toggle
│       ├── Paging            the pager
│       └── Recordset         the grid or list of records
│           └── Record        one per row
│               ├── Field     one per entry in fields
│               └── Action    one per entry in actions
└── FieldHeaderContainer      column headers, List view only
```

Every box in that tree except `DataManager` inherits from [Component](/classes-and-apis/component.js), so `setText`, `addClass`, `getChild`, `remove` and the rest work the same way on all of them.

## The config mirrors the tree

Where a component sits in the tree is where its options sit in the config:

| Component | Configured by                                                     |
| --------- | ----------------------------------------------------------------- |
| Dashboard | the top level — `language`, `profile`, `initialActiveTab`         |
| Tab       | an entry in [`tabs`](/configuration-options/config/tabs)          |
| Record    | [`recordSettings`](/configuration-options/config/record-settings) |
| Field     | an entry in [`fields`](/configuration-options/config/fields)      |
| Action    | an entry in [`actions`](/configuration-options/config/actions)    |

## Walking the tree

```javascript
const tab       = dashboard.getChild('User Profiles');
const recordset = tab.getChild('Recordset');
const record    = recordset.children.records[0];
const field     = record.getChild('Name');
```

Inside a [callback](/configuration-options/config/callbacks) you rarely need to walk anything — the context object hands you `component`, `record`, `el` and `dashboard` directly.

## Lifecycle

1. **init** — config is copied onto the component; deprecated keys are rewritten; computed properties are set aside.
2. **render** — the template is cloned and the DOM node created.
3. **processEvents** — `visibility`, `icon` and `url` are resolved, click handlers are attached, then `onRender` fires.
4. **mount** — the component is appended to its parent, and `onMount` fires.
5. **removal** — `remove()` consults `onBeforeRemove`, then `delete()` detaches the node.

A record is built once for Card view and once for List view, so `onRender` fires for each — keep it idempotent.

## The classes

| Class       | Page                                               |
| ----------- | -------------------------------------------------- |
| Component   | [Component.js](/classes-and-apis/component.js)     |
| Dashboard   | [Dashboard.js](/classes-and-apis/dashboard.js)     |
| Tabs        | [Tabs.js](/classes-and-apis/tabs.js)               |
| Tab         | [Tab.js](/classes-and-apis/tab.js)                 |
| Recordset   | [Recordset.js](/classes-and-apis/recordset.js)     |
| DataManager | [DataManager.js](/classes-and-apis/datamanager.js) |


# Component.js

`Component` is the base class every other class inherits from — `Dashboard`, `Tab`, `Recordset`, `Record`, `Field`, `Action` and the tools. Anything documented here is available on all of them.

You get a component in three ways: from a [callback](/configuration-options/config/callbacks) context, from `dashboard.getChild(...)`, or as the value returned by `new FutureLabs.Dashboard(...)`.

## Properties

| Property         | Type      | What it is                                                                                                                         |
| ---------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `data`           | any       | The component's own data. On a Field this is the field's value; on an Action or Record it is the row. Prefer `value` and `record`. |
| `value`          | any       | The field's value. Fields only.                                                                                                    |
| `record`         | object    | The whole row, as a plain data object. Set on every component.                                                                     |
| `el`             | Element   | The rendered DOM node. Alias of `object`.                                                                                          |
| `component`      | Component | The component itself. Present so `({ component })` reads naturally in callbacks.                                                   |
| `dashboard`      | Dashboard | The Dashboard this component belongs to.                                                                                           |
| `children`       | object    | Child components, keyed by container name.                                                                                         |
| `name`           | string    | The component's configured name.                                                                                                   |
| `translatedName` | string    | `name` resolved for the active language.                                                                                           |
| `language`       | string    | The active language code, e.g. `'en-US'`.                                                                                          |
| `id`             | string    | Readable id, generated from the name unless you set one.                                                                           |
| `uid`            | string    | Unique id — `id` plus a random suffix.                                                                                             |
| `config`         | object    | A clone of the config this component was built from.                                                                               |
| `visibility`     | string    | Resolved visibility: `'show'`, `'enable'`, `'disable'` or `'hide'`.                                                                |
| `template`       | Template  | The template backing this component.                                                                                               |
| `objects`        | object    | Named DOM nodes inside the template (`item`, `itemText`, `itemIcon`, `itemLink`, `container`…).                                    |

## Content

<details>

<summary><code>setText(value, selectorKey)</code></summary>

Sets the text of the component, or of a named node inside it. HTML is accepted.

```javascript
field.setText('Overdue');
field.setText('Balance', 'itemTitle');   // the field's label
```

</details>

<details>

<summary><code>setIcon(value, selectorKey)</code> · <code>removeIcon(selectorKey)</code></summary>

Applies or clears an icon class on the component's icon node.

```javascript
action.setIcon('fas fa-check');
action.removeIcon();
```

</details>

<details>

<summary><code>setImage(value, selectorKey)</code> · <code>setBackgroundImage(value, selectorKey, height)</code></summary>

`setImage` points an `<img>` at a URL. `setBackgroundImage` sets a CSS background and accepts a height, which is how a record's `image` config is applied.

```javascript
record.setBackgroundImage('/img/avatar.jpg', null, { height: '200px' });
```

</details>

<details>

<summary><code>setLink(value, selectorKey, target)</code></summary>

Wraps the component in an anchor pointing at `value`. Creating the anchor may replace the component's own node, which the component tracks for you.

Prefer the [`url`](/configuration-options/config/fields#linking) config property — it handles conditional links and cleanup.

</details>

## Classes

<details>

<summary><code>addClass(value, selectorKey)</code> · <code>removeClass(value, selectorKey)</code></summary>

```javascript
Status: {
  onRender: ({ component, record }) => {
    if (record.Balance > 0) component.addClass('is-overdue');
  }
}
```

Pass a `selectorKey` to target a named node rather than the component root.

</details>

## Tree

<details>

<summary><code>getChild(name, containerKey)</code></summary>

Finds a direct child by its `name`. Pass `containerKey` to search a single container.

```javascript
const tab = dashboard.getChild('User Profiles');
const recordset = tab.getChild('Recordset');
```

</details>

<details>

<summary><code>getChildById(id)</code></summary>

Finds a descendant by `id` or `uid`, searching every container.

</details>

<details>

<summary><code>append(child, containerSelector)</code> · <code>prepend(child, containerSelector)</code></summary>

Adds a child component into a named container. This is what fires the child's [`onMount`](/configuration-options/config/callbacks).

</details>

<details>

<summary><code>appendTo(parent)</code></summary>

Appends this component into another component or DOM node.

</details>

## Removal

<details>

<summary><code>remove()</code></summary>

Removes the component, **after** consulting [`onBeforeRemove`](/configuration-options/config/callbacks). If that hook returns `false` nothing happens; if it returns a promise, removal waits for it.

```javascript
const action = record.getChild('Delete');
action.remove();       // may prompt, may be cancelled
```

</details>

<details>

<summary><code>removeChildren(selectorKey)</code></summary>

Removes every child, or only those in one container. Also guarded by `onBeforeRemove`.

</details>

<details>

<summary><code>delete()</code></summary>

Removes the component immediately, with no guard. `remove()` calls this once the guard allows it.

</details>

## Loading and animation

<details>

<summary><code>showLoader()</code> · <code>hideLoader()</code></summary>

Shows or hides the component's loading spinner. Tabs use these around a fetch.

</details>

<details>

<summary><code>fadeOut()</code> · <code>fadeInLeft()</code> · <code>fadeLeft()</code> · <code>fadeRight()</code></summary>

Applies the built-in transition classes, used when moving between pages and tabs.

</details>

## Callback plumbing

These back the [callback contract](/configuration-options/config/callbacks). You rarely call them directly, but they are what makes any config value able to be a function.

<details>

<summary><code>resolve(name, fallback)</code></summary>

Resolves a noun property. If it was configured as a function, calls it with the context object and returns the result; `undefined` returns `fallback`. If it was a static value, returns that. A callback that throws is contained and `fallback` is used.

```javascript
const icon = field.resolve('icon', null);
```

</details>

<details>

<summary><code>trigger(name, extra)</code></summary>

Invokes a verb hook with the same context convention, returning whatever the hook returned.

```javascript
const outcome = component.trigger('onBeforeRemove');
```

</details>

## Statics

| Static                                       | What it does                                                                                                |
| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `Component.COMPUTED_PROPS`                   | The noun properties that may be functions: `value`, `icon`, `url`, `visibility`, `class`, `style`, `width`. |
| `Component.RENAMED_PROPS`                    | Deprecated config keys mapped to their replacements.                                                        |
| `Component.NON_CASCADING_PROPS`              | Hooks stripped from derived components such as field headers and sorting items.                             |
| `Component.stripHooks(config)`               | Removes those hooks from a config object.                                                                   |
| `Component.normalizeContract(config, where)` | Rewrites deprecated keys onto their replacements and warns once per key.                                    |
| `Component.generateRandomId(name)`           | Builds a `uid` from a readable id.                                                                          |


# Dashboard.js

`Dashboard` is the entry point and the root of the component tree. It inherits everything on [Component.js](/classes-and-apis/component.js).

```javascript
var dashboard = new FutureLabs.Dashboard({
  language: 'en-US',
  config: { /* ... */ },
  data: { /* ... */ },
  templateURL: 'dashboardjs/dashboard.html',
  appendTo: '.dashboard-container'
});
```

## Constructor settings

| Setting              | Type              | What it does                                                                                                                                                 |
| -------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `config`             | object            | The dashboard definition. Optional — omit it and one tab named `All Records` is created. See [Config](/configuration-options/config).                        |
| `data`               | array \| object   | The records. An array for a single tab, or an object keyed by tab name. See [Data](/configuration-options/data).                                             |
| `language`           | string            | Active language code. Defaults to `'en-US'`.                                                                                                                 |
| `templateURL`        | string            | Loads the HTML template from a URL instead of using the markup already on the page. See [templateURL](/configuration-options/templateurl).                   |
| `appendTo`           | string \| Element | Where to mount. See [appendTo](/configuration-options/appendto).                                                                                             |
| `useExistingElement` | boolean           | Defaults to `true` on the Dashboard — it modifies the template already in the document rather than cloning it. Passing `appendTo` overrides this to `false`. |
| `templateManager`    | TemplateManager   | Supply your own template manager. One is created if omitted.                                                                                                 |
| `selectors`          | object            | Override the default template selectors.                                                                                                                     |

## Properties

| Property          | Type    | What it is                                                                  |
| ----------------- | ------- | --------------------------------------------------------------------------- |
| `tabs`            | Tabs    | The tab strip. See [Tabs.js](/classes-and-apis/tabs.js).                    |
| `children`        | object  | Child components by container.                                              |
| `language`        | string  | Active language code.                                                       |
| `config`          | object  | A clone of the config passed in.                                            |
| `loadingTemplate` | Promise | Resolves once the template has loaded. `await` it when using `templateURL`. |

## Methods

<details>

<summary><code>switchView(viewMode)</code></summary>

Switches every recordset between Card and List view. Anything other than `'cards'` (case-insensitive) is treated as list.

```javascript
dashboard.switchView('List');
dashboard.switchView('Cards');
```

Per-tab defaults are set with the tab's [`viewMode`](/dashboard-tools/view-modes).

</details>

<details>

<summary><code>getChild(name)</code></summary>

Returns a tab by name.

```javascript
const invoices = dashboard.getChild('Invoices');
```

</details>

<details>

<summary><code>loadDashboard()</code></summary>

Builds the tab strip and recordsets from `config.tabs`. Called for you by the constructor.

</details>

<details>

<summary><code>loadHTML(templateURL, appendTo)</code></summary>

Fetches an HTML template and mounts it. Called for you when `templateURL` is set.

</details>

## Waiting for the dashboard

The constructor returns immediately; rendering is asynchronous. When you pass `templateURL`, await `loadingTemplate` before touching the tree.

```javascript
var dashboard = new FutureLabs.Dashboard({
  data: data,
  templateURL: 'dashboardjs/dashboard.html',
  appendTo: '.dashboard-container'
});

await dashboard.loadingTemplate;
dashboard.switchView('List');
```

## Reaching a tab's data

Each tab owns a [DataManager](/classes-and-apis/datamanager.js), which is where paging, sorting, filtering and fetching live.

```javascript
const tab = dashboard.getChild('User Profiles');
tab.dataManager.goToPage(2);
tab.dataManager.sort({ sortBy: 'Name', sortDirection: 'asc' });
tab.dataManager.refresh();
```


# Tabs.js

`Tabs` is the strip that holds every [Tab](/classes-and-apis/tab.js). One is created for you from `config.tabs`; you rarely construct it yourself.

```javascript
const tabs = dashboard.tabs;
```

Inherits everything on [Component.js](/classes-and-apis/component.js).

## Properties

| Property        | Type      | What it is                           |
| --------------- | --------- | ------------------------------------ |
| `children.tabs` | Tab\[]    | Every tab, in config order.          |
| `dashboard`     | Dashboard | The dashboard this strip belongs to. |

## Methods

<details>

<summary><code>getChild(name)</code></summary>

Returns a tab by name. `dashboard.getChild(name)` is the shorter route to the same object.

</details>

<details>

<summary><code>setActive(tab)</code></summary>

Opens one tab and closes the rest. Prefer `tab.setActive(true)`.

</details>

## Which tab opens first

`initialActiveTab` names it. Without it, the first entry in `config.tabs` opens.

```javascript
config: {
  initialActiveTab: 'Invoices',
  tabs: { 'User Profiles': { /* ... */ }, 'Invoices': { /* ... */ } }
}
```

## The overflow menu

When tabs do not fit, the strip collapses into a dropdown. That is handled by the template and CSS — there is nothing to configure.

## Iterating

```javascript
dashboard.tabs.children.tabs.forEach((tab) => {
  console.log(tab.name, tab.dataManager.count);
});
```


# Tab.js

One `Tab` per entry in [`config.tabs`](/configuration-options/config/tabs). A tab owns the recordset for its data plus the tools around it — sorting, filtering, paging and the view switcher — and holds the [DataManager](/classes-and-apis/datamanager.js) those tools drive.

```javascript
const tab = dashboard.getChild('User Profiles');
```

Inherits everything on [Component.js](/classes-and-apis/component.js).

## Properties

| Property         | Type             | What it is                                                  |
| ---------------- | ---------------- | ----------------------------------------------------------- |
| `dataManager`    | DataManager      | Rows, paging, sorting, filtering and fetching for this tab. |
| `recordSettings` | object           | The record configuration for this tab.                      |
| `fields`         | object           | Field configuration, resolved for the active language.      |
| `icon`           | string           | The tab's icon class.                                       |
| `description`    | string \| object | Blurb shown above the records. Translatable.                |
| `active`         | boolean          | Whether this is the open tab.                               |
| `pagination`     | Paging           | The pager component.                                        |
| `name`           | string           | The tab's key in `config.tabs`.                             |
| `translatedName` | string           | That name in the active language.                           |

## Methods

<details>

<summary><code>setActive(active)</code></summary>

Opens this tab and closes the others.

```javascript
dashboard.getChild('Invoices').setActive(true);
```

</details>

<details>

<summary><code>refresh()</code></summary>

Re-renders the recordset from the DataManager, re-fetching first when the tab is asynchronous.

</details>

<details>

<summary><code>refreshCount()</code></summary>

Updates the tab's badge without reloading the rows — a count-only load.

</details>

<details>

<summary><code>setView(viewMode)</code></summary>

Switches this tab between `'Cards'` and `'List'`. See [View Modes](/dashboard-tools/view-modes).

</details>

<details>

<summary><code>setSorting(sorting)</code> · <code>setFiltering(filtering)</code> · <code>setPagination()</code></summary>

Push state into the tab's tools and re-render. These are what the sort control, keyword box and pager call.

</details>

<details>

<summary><code>setRecordset(data)</code></summary>

Replaces the rendered records with a new set of rows.

</details>

<details>

<summary><code>getDescription()</code></summary>

Returns the description resolved for the active language.

</details>

<details>

<summary><code>showLoader()</code> · <code>hideLoader()</code></summary>

Shows or hides this tab's spinner. Called around every fetch.

</details>

## Example

```javascript
const tab = dashboard.getChild('Invoices');

tab.setActive(true);
tab.setView('List');
tab.dataManager.sort({ sortBy: 'Balance', sortDirection: 'desc' });
tab.refresh();
```


# Recordset.js

`Recordset` renders the rows inside a tab — as a grid of cards or as a list. It builds one [Record](/configuration-options/config/record-settings) per row from the tab's `recordSettings`.

Inherits everything on [Component.js](/classes-and-apis/component.js).

## Properties

| Property           | Type        | What it is                                          |
| ------------------ | ----------- | --------------------------------------------------- |
| `children.records` | Record\[]   | The rendered records, in display order.             |
| `recordSettings`   | object      | The record configuration for this tab.              |
| `recordsGrid`      | object      | CSS Grid pairs laying out the records in Card view. |
| `dataManager`      | DataManager | The tab's data manager.                             |
| `data`             | array       | The rows currently rendered — one page's worth.     |

## Methods

<details>

<summary><code>switchView(viewMode)</code></summary>

Switches this recordset between `'Cards'` and `'List'`.

</details>

<details>

<summary><code>refresh()</code></summary>

Rebuilds every record from the current page of data.

</details>

<details>

<summary><code>removeChildren()</code></summary>

Removes every record. Guarded by `onBeforeRemove` if one is configured.

</details>

<details>

<summary><code>setActionsListViewWidth()</code></summary>

Sizes the actions column in List view so buttons line up across rows.

</details>

## Reaching a record

```javascript
const recordset = dashboard.getChild('User Profiles').getChild('Recordset');

recordset.children.records.forEach((record) => {
  if (record.record.Balance > 0) record.addClass('is-overdue');
});
```

Inside a callback the same thing is one line, because the context already holds the row and the node:

```javascript
recordSettings: {
  onRender: ({ record, el }) => el.classList.toggle('is-overdue', record.Balance > 0)
}
```

## Layout

`recordsGrid` takes raw CSS Grid property/value pairs and applies them to the records container in Card view:

```javascript
recordsGrid: {
  'grid-template-columns': '1fr 1fr 1fr',
  'gap': '20px',
  'justify-items': 'stretch'
}
```

Field layout *within* a record is `fieldsGrid`, on [Record Settings](/configuration-options/config/record-settings).


# DataManager.js

Every tab owns a `DataManager`. It holds the rows and everything that changes which rows are visible: paging, sorting, filtering, keyword search, and fetching from a server.

```javascript
const dm = dashboard.getChild('User Profiles').dataManager;
```

Unlike the rest of the library it is **not** a Component — it has no DOM of its own.

## Properties

| Property        | Type     | What it is                                                                     |
| --------------- | -------- | ------------------------------------------------------------------------------ |
| `data`          | object   | The pipeline: `raw`, `searched`, `filtered`, `sorted`, `paged`.                |
| `count`         | number   | Total number of records. From the server when fetching.                        |
| `page`          | number   | Current page. 1-based.                                                         |
| `pages`         | number   | Total pages, derived from `count` and `itemsPerPage`.                          |
| `itemsPerPage`  | number   | Records per page. Defaults to `12`.                                            |
| `sorting`       | object   | `{ sortBy, sortDirection, sortFieldText }`.                                    |
| `filtering`     | object   | `{ keywords: [] }`.                                                            |
| `search`        | object   | `{ parameters: [], options: { enableSpecialCharacters, wholeWordSearch } }`.   |
| `fetch`         | object   | Fetch configuration. See [Fetch API](/configuration-options/config/fetch-api). |
| `fetchFunction` | function | Supply your own loader instead of `fetch`.                                     |
| `tabName`       | string   | Sent with every request so the server knows which tab is asking.               |

The pipeline runs in that order — search, then filter, then sort, then page — so `data.paged` is what ends up on screen.

## Reading data

<details>

<summary><code>getData()</code></summary>

Returns the rows for the current page, after search, filtering and sorting.

</details>

<details>

<summary><code>getFieldsFromData()</code></summary>

Infers field keys from the first record. This is how a dashboard with no `fields` config still renders columns.

</details>

## Paging

<details>

<summary><code>goToPage(page)</code></summary>

Moves to a page, fetching it if the tab loads asynchronously. Pages are 1-based.

```javascript
dm.goToPage(3);
```

</details>

## Sorting

<details>

<summary><code>sort(sorting)</code> · <code>setSorting(sorting)</code></summary>

`sort()` applies a sort and re-renders. `setSorting()` records it without re-running the pipeline.

```javascript
dm.sort({ sortBy: 'Age', sortDirection: 'asc' });
```

| Key             | What it is                                                                    |
| --------------- | ----------------------------------------------------------------------------- |
| `sortBy`        | The data key to sort on.                                                      |
| `sortDirection` | `DataManager.SORTING.ASC` (`'asc'`) or `DataManager.SORTING.DESC` (`'desc'`). |
| `sortFieldText` | The label shown in the sort control.                                          |

</details>

<details>

<summary><code>toggleSorting()</code></summary>

Flips the current direction between ascending and descending.

</details>

## Filtering and search

<details>

<summary><code>filter(filtering)</code> · <code>setFiltering(filtering)</code></summary>

Applies a keyword filter. `filtering` is `{ keywords: [...] }`.

```javascript
dm.filter({ keywords: ['Married'] });
```

</details>

<details>

<summary><code>addKeyword(keyword)</code> · <code>addFilterKeyword(keyword)</code></summary>

Adds a single keyword to the active filter.

</details>

<details>

<summary><code>doSearch(searchParameters)</code> · <code>setSearch(searchParameters)</code></summary>

Runs a structured search. Options are `enableSpecialCharacters` and `wholeWordSearch`.

</details>

## Refreshing

<details>

<summary><code>refresh()</code></summary>

Re-runs the pipeline, re-fetching first if the tab is asynchronous.

</details>

<details>

<summary><code>reset()</code></summary>

Clears search, filtering and sorting and returns to page 1.

</details>

<details>

<summary><code>setData(data, count)</code></summary>

Replaces the rows. Pass `count` when the total differs from the array's length, which it does whenever the server pages for you.

```javascript
dm.setData(rows, 240);
```

</details>

<details>

<summary><code>setCount(count)</code></summary>

Updates the total **without** touching the rows on screen. Used by count-only loads — routing those through `setData()` would blank a populated tab with the empty list such a response carries.

</details>

<details>

<summary><code>load(countOnly)</code></summary>

Performs the fetch. With `countOnly` it asks only for a total.

</details>

## Concurrency

Overlapping refreshes used to interleave and land out of order. Two guards prevent that, and they are worth knowing about if you drive the manager yourself:

* **`runExclusive`** — only one refresh or page change runs at a time. A request arriving mid-flight is queued as `refreshAgain` and runs the moment the active one finishes; further requests replace that queued one rather than stacking up.
* **`runFetchExclusive`** — chains fetches so only one is ever in flight, with `fetchQueue` holding the tail of the chain.

The practical consequence: rapid clicks on pagination or sorting settle on the last request rather than racing.

## Statics

| Static                                          | Value                                                                                                                                         |
| ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `DataManager.SORTING.ASC`                       | `'asc'`                                                                                                                                       |
| `DataManager.SORTING.DESC`                      | `'desc'`                                                                                                                                      |
| `DataManager.normaliseResponse(res, countOnly)` | Coerces a response into `{ data, count }`. Accepts a bare array and counts it; reports anything unusable by name instead of failing silently. |


