owc-data-detail renders one record as a compact label/value grid. It is the
single-record companion to owc-table: use the table to scan many rows, and use
Data Detail to show or edit the selected row.
The component is configured with a data object and a nested columns array. Each
inner array is one visual column; each item inside that array is one label/value row.
Items resolve their value from field (including dot paths such as
client.firstName and JavaScript getters), then optionally format, edit, hide, or
expand that value.
The most common item shape is:
{ label: 'First Name', field: 'firstName' }
Add type: 'editable' for click-to-edit values, formatter for display formatting,
visible for conditional rows, and type: 'expandable' plus contentExpanded when a
row should open a larger detail area.
Start with data and one or more visual columns. This example renders two visual
columns: names on the left, birth data on the right. The date row uses the built-in
date formatter, but the source value in data.dateOfBirth stays unchanged.
const basicColumns = [
[
{ label: 'First Name', field: 'firstName' },
{ label: 'Last Name', field: 'lastName' },
],
[
{ label: 'Date of Birth', field: 'dateOfBirth', formatter: 'date' },
{ label: 'Age', field: 'age' },
],
];
export const basicUsageDemo = () => html`
<owc-data-detail .data=${data} .columns=${basicColumns}></owc-data-detail>
`;
This example combines the main features in one component:
First Name, Last Name, Date of Birth)Favorite Planet)Family Members, Email)age, primaryEmail, primaryFamilyMember)Use it as a reference when you need several features together. The smaller demos below show each concept in isolation.
const kitchenSinkOptions = {
handleUpdate: ({ data, field, config, value, autoSetData }) => {
console.log({ field, value });
autoSetData();
},
columns: [
[
{
label: 'First Name',
field: 'firstName',
type: 'editable',
editableOptions: {
inputOptions: {
fallbackValue: 'thomas',
},
},
},
{
label: 'Last Name',
field: 'lastName',
type: 'editable',
editableOptions: {
required: true,
},
},
{
label: 'Family Members',
field: 'primaryFamilyMember',
type: 'expandable',
contentExpanded: data =>
html`<owc-table
.data=${data.familyMemberList}
.handleInsert=${() => {
return new ClientRelationShip();
}}
.columns=${[
{
label: 'First Name',
field: 'client.firstName',
type: 'editable',
},
{
label: 'Last Name',
field: 'client.lastName',
type: 'editable',
},
{
label: 'Type',
field: 'type',
type: 'editable',
editableOptions: {
type: 'autocomplete',
data: [
{ value: 'husband/wife', label: 'Husband/Wife/Partner' },
{ value: 'parent/child', label: 'Parent/Child' },
],
},
},
]}
></owc-table>`,
labelBadge: data => data.familyMemberList.length,
},
{
label: 'Favorite Planet',
field: 'favoritePlanet',
contentSuffix: () => html`<wa-icon name="rocket-takeoff"></wa-icon>`,
type: 'editable',
editableOptions: {
type: 'autocomplete',
data: PLANET_LIST.map(elm => ({ label: elm.label, value: elm.value })),
},
},
],
[
{
label: 'Email',
field: 'primaryEmail',
type: 'expandable',
contentExpanded: data =>
html`<owc-table
.data=${data.emailList}
.handleInsert=${() => {
return { type: 'public', email: '' };
}}
.handleUpdate=${({ autoSetData, data }) => {
autoSetData();
}}
.columns=${[
{ label: 'E-Mail', field: 'email', type: 'editable' },
{
label: 'Type',
field: 'type',
type: 'editable',
editableOptions: {
type: 'autocomplete',
data: [
{ value: 'private', label: 'Private (hidden)' },
{ value: 'public', label: 'Public' },
],
},
},
]}
></owc-table>`,
labelBadge: data => data.emailList.length,
},
{
label: 'Date of Birth',
field: 'dateOfBirth',
type: 'editable',
editableOptions: {
inputOptions: {
type: 'date',
},
},
},
{ label: 'Age', field: 'age', formatter: data => `${data.age} Years` },
],
],
};
export const kitchenSinkDemo = () => html`
<owc-data-detail
.data=${client}
fallbackValue="-"
${spreadProps(/**@type {{[key: string]: unknown}}*/ (kitchenSinkOptions))}
></owc-data-detail>
`;
columns is an array of visual columns. Each visual column is an array of rows. The
component renders rows by index, so the first item from each visual column appears on
the first grid row, the second item from each visual column appears on the second grid
row, and so on.
This makes it easy to split a large detail view into compact side-by-side groups while keeping the row order explicit.
A single inner array creates a simple vertical label/value list. This is the clearest layout for short records or narrow containers.
const oneColumn = [
[
{ label: 'First Name', field: 'firstName' },
{ label: 'Last Name', field: 'lastName' },
{ label: 'Age', field: 'age' },
],
];
export const oneColumnsDemo = () => html`
<owc-data-detail .data=${data} .columns=${oneColumn}></owc-data-detail>
`;
Use two inner arrays to place two groups side by side. Here First Name and Last Name
share the first rendered row, and Age occupies the second row in the first visual
column.
const twoColumns = [
[
{ label: 'First Name', field: 'firstName' },
{ label: 'Age', field: 'age' },
],
[{ label: 'Last Name', field: 'lastName' }],
];
export const twoColumnsDemo = () => html`
<owc-data-detail .data=${data} .columns=${twoColumns}></owc-data-detail>
`;
Additional inner arrays add more visual columns. Keep the number of columns low when labels or values are long; the component is intentionally compact and does not wrap labels by default.
const threeColumns = [
[
{ label: 'First Name', field: 'firstName' },
{ label: 'Age', field: 'age' },
],
[{ label: 'Last Name', field: 'lastName' }],
[{ label: 'Last Name', field: 'lastName' }],
];
export const threeColumnsDemo = () => html`
<owc-data-detail .data=${data} .columns=${threeColumns}></owc-data-detail>
`;
The default render mode is plain HTML content resolved from field. Setting
type: 'html' is explicit and useful when you want the column config to document that
the value is display-only.
const contentType = [
[
{ label: 'First Name', field: 'firstName', type: 'html' },
{ label: 'Last Name', field: 'lastName', type: 'html' },
{ label: 'Age', field: 'age', type: 'html' },
],
];
export const contentTypeDemo = () => html`
<owc-data-detail .data=${data} .columns=${contentType}></owc-data-detail>
`;
Set type: 'editable' to render a click-editable value. When the user submits a value,
owc-data-detail calls handleUpdate with { data, field, value, config, autoSetData }.
If no handleUpdate is provided, the component writes the submitted value into data
itself.
Editable fields use the click-editable input by default. You can also set
editableOptions.type: 'input' explicitly, as shown here.
const inputType = [
[
{
label: 'First Name',
field: 'firstName',
type: 'editable',
editableOptions: {
type: 'input',
},
},
{
label: 'Last Name',
field: 'lastName',
type: 'editable',
editableOptions: {
type: 'input',
},
},
{
label: 'Age',
field: 'age',
type: 'editable',
editableOptions: {
type: 'input',
},
},
],
];
export const inputTypeDemo = () => html`
<owc-data-detail .data=${data} .columns=${inputType}></owc-data-detail>
`;
This is the shortest editable configuration. Because no editableOptions.type is set,
the editable type defaults to a text input.
const editableTextColumns = [[{ label: 'First Name', field: 'firstName', type: 'editable' }]];
export const editableTextDemo = () => html`
<owc-data-detail .data=${data} .columns=${editableTextColumns}></owc-data-detail>
`;
Pass options through editableOptions.inputOptions to configure the underlying
click-editable input. A numeric input is useful when browser-level number controls or
validation are desired.
const editableNumberColumns = [
[
{
label: 'Age',
field: 'age',
type: 'editable',
editableOptions: { type: 'input', inputOptions: { type: 'number' } },
},
],
];
export const editableNumberDemo = () => html`
<owc-data-detail .data=${data} .columns=${editableNumberColumns}></owc-data-detail>
`;
The same input options can switch the editor to a date input. The stored value is still
the submitted input value; add handleUpdate when you need to normalize it before
writing it back to your model.
const editableDateColumns = [
[
{
label: 'Date of Birth',
field: 'dateOfBirth',
type: 'editable',
editableOptions: { type: 'input', inputOptions: { type: 'date' } },
},
],
];
export const editableDateDemo = () => html`
<owc-data-detail .data=${data} .columns=${editableDateColumns}></owc-data-detail>
`;
Use editableOptions.type: 'autocomplete' with a data array when the value should be
selected from known options. The display value is the stored value, while the editor
uses the labels from the autocomplete data.
const autocompleteColumns = [
[
{
label: 'Planet',
field: 'planet',
type: 'editable',
editableOptions: {
type: 'autocomplete',
data: PLANET_LIST.map(elm => ({ label: elm.label, value: elm.value })),
},
},
],
];
export const autocompleteTypeDemo = () => html`
<owc-data-detail .data=${data} .columns=${autocompleteColumns}></owc-data-detail>
`;
Expandable rows are for values that need a compact summary plus a larger detail area.
The label becomes a toggle, the normal value stays visible, and contentExpanded
renders below the grid row. Only one expandable field is opened by a label click at a
time; control the initial state with openColumns.
This row opens hard-coded content below the detail grid. In real usage the expanded content can be any Lit template, including forms, charts, or another component.
const expandableHardCodedColumns = [
[
{
label: 'First Name',
field: 'firstName',
type: 'expandable',
contentExpanded: () => html`<p>some expanded stuff</p>`,
},
],
];
export const expandableHardCodedDemo = () => html`
<owc-data-detail .data=${data} .columns=${expandableHardCodedColumns}></owc-data-detail>
`;
Expandable rows can live in different visual columns. Clicking one expandable label closes the previously opened expandable row, because the component stores a single open field when users toggle labels.
const expandableSharedColumns = [
[
{
label: 'First Name',
field: 'firstName',
type: 'expandable',
contentExpanded: () => html`<p>some info about First Name</p>`,
},
],
[
{
label: 'Last Name',
field: 'lastName',
type: 'expandable',
contentExpanded: () => html`<p>some info about Last Name</p>`,
},
],
];
export const expandableSharedDemo = () => html`
<owc-data-detail .data=${data} .columns=${expandableSharedColumns}></owc-data-detail>
`;
Set .openColumns when the detail view should start with a specific expandable row
already open.
const expandableOpenedColumns = [
[
{
label: 'First Name',
field: 'firstName',
type: 'expandable',
contentExpanded: () => html`<p>some expanded stuff</p>`,
},
],
];
export const expandableOpenedDemo = () => html`
<owc-data-detail
.data=${data}
.columns=${expandableOpenedColumns}
.openColumns=${['firstName']}
></owc-data-detail>
`;
contentSuffix renders additional content after the value. Use it for small actions,
icons, units, or status markers that belong to the value but should not replace the
value itself.
const contentSuffixColumns = [
[
{
label: 'Telefonnummer',
contentSuffix: () => html`<wa-icon name="pencil"></wa-icon>`,
field: 'phone',
type: 'html',
},
],
];
export const contentSuffixColumnsDemo = () => html`
<owc-data-detail .data=${data} .columns=${contentSuffixColumns}></owc-data-detail>
`;
Formatters transform the displayed value while leaving the underlying data object
unchanged. Built-in formatter names include date, datetime, currency, number,
percent, email, tickCross, and checkbox.
const builtInFormatterColumns = [
[
{
label: 'Date of Birth',
field: 'dateOfBirth',
formatter: 'date',
},
],
];
export const builtinFormatter = () => html`
<owc-data-detail .data=${data} .columns=${builtInFormatterColumns}></owc-data-detail>
`;
The built-in date, datetime, number, currency, and percent formatters use formatter instances from the component. Override those properties when the same formatter name should render with different locale or formatting rules.
const overrideBuiltInFormatterColumns = [
[
{
label: 'Date of Birth',
field: 'dateOfBirth',
formatter: 'date',
},
],
];
export const overrideBuiltinFormatter = () => html`
<owc-data-detail
.data=${data}
.columns=${overrideBuiltInFormatterColumns}
.dateFormatter=${new Intl.DateTimeFormat('de', {
day: '2-digit',
month: 'long',
year: 'numeric',
})}
></owc-data-detail>
`;
Use a formatter function when the display value depends on more than a named formatter. The function receives the full data record, so it can combine fields, add custom markup, or return a fallback string.
const formatter = [
[
{
label: 'Date of Birth',
field: 'dateOfBirth',
formatter: data => {
const dateString = new Intl.DateTimeFormat('de', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
}).format(new Date(data.dateOfBirth));
return `📆 ${dateString}`;
},
},
],
];
export const formatterDemo = () => html`
<owc-data-detail .data=${data} .columns=${formatter}></owc-data-detail>
`;
Set visible: false to hide a row unconditionally. Hidden rows are removed before the
grid row count is calculated.
const visibleItems = [
[
{
label: 'Planet',
field: 'planet',
visible: false,
},
{
label: 'Age',
field: 'age',
},
],
];
export const visibleDemo = () => html`
<owc-data-detail .data=${data} .columns=${visibleItems}></owc-data-detail>
`;
visible can also be a function of the current record. This is useful for fields that
only apply to some records. In this example the first detail view hides First Name
because age is 12; the second one shows it because the data override sets age to
40.
const visibleFunctionItems = [
[
{
label: 'First Name',
field: 'firstName',
visible: data => data.age > 20,
},
{
label: 'Age',
field: 'age',
},
],
];
export const visibleFunctionDemo = () => html`
<owc-data-detail .data=${data} .columns=${visibleFunctionItems}></owc-data-detail>
<hr />
<owc-data-detail .data=${{ ...data, age: 40 }} .columns=${visibleFunctionItems}></owc-data-detail>
`;
| Attribute | Property | Type | Default | Description |
|---|---|---|---|---|
| - | data | T | {} | The record to display. |
| - | columns | OwcDataDetailColumns<T> | [] | Array of columns; each column is an array of items (see below). |
| - | openColumns | Array<Field<T>> | [] | Fields whose expandable content is open. Managed on label clicks (single open). |
fallbackValue | fallbackValue | string | '-' | Fallback shown in editable cells without a value. |
| - | handleUpdate | handleUpdate<T> | - | Called when an editable cell is submitted ({ data, field, value, config, autoSetData }). |
| - | dateFormatter, dateTimeFormatter, currencyFormatter, numberFormatter, percentFormatter | Intl.*Format | German locale | Formatters used by the built-in formatter names. |
OwcDataDetailItem<T>)#| Field | Type | Description |
|---|---|---|
label | string | (data: T) => TemplateResult | string | The row label. |
field | Field<T> | Field path into data (dot paths like client.firstName work). |
type | 'html' | 'string' | 'editable' | 'expandable' | How the value renders; default is plain content. |
formatter | built-in name or (row, options) => ... | Built-ins: date, datetime, currency, number, percent, email, tickCross, checkbox. |
editableOptions | EditableOptions<T> | For type: 'editable': input type (input, textarea, autocomplete, checkbox), options, required. |
contentExpanded | (data: T) => TemplateResult | For type: 'expandable': the expanded content (e.g. a nested owc-table). |
labelBadge | (data: T) => TemplateResult | string | number | Small badge rendered next to the label. |
contentSuffix | (data: T) => TemplateResult | string | number | Content rendered after the value. |
visible | boolean | (data: T) => boolean | Hide/show the item; defaults to visible. |
| Field | Type | Description |
|---|---|---|
type | 'input' | 'textarea' | 'autocomplete' | 'checkbox' | Editor to render for type: 'editable'; defaults to input. |
inputOptions | object | Options forwarded to the click-editable input, textarea, or autocomplete. |
insertInputOptions | object | Options used when a surrounding table renders the value as a new insert row. |
data | Array<{ label: string, value: string }> | Autocomplete options. |
dataFn | (row: T) => Array<{ label: string, value: string }> | Builds autocomplete options from the current row. |
required | boolean | Marks the field as required for editable update helpers. |
The types are importable from @open-wc/components/OwcDataDetail.types.js.