Detail

The <smart-detail> component renders a single model field value by delegating to a sub-component chosen by FieldType. It selects a sub-component based on the FieldType from an internal baseMap and an optional extendMap injected via DETAIL_FIELD_COMPONENTS_TOKEN. It renders the label (via ModelLabelPipe), the value (NgComponentOutlet on the selected sub-component), or a skeleton when item() returns undefined.


Usage

<smart-detail [type]="type" [options]="nameOptions" />
<smart-detail [type]="type" [options]="emailOptions" />

Components

DetailComponent (<smart-detail>)

Main wrapper component. Selects a sub-component from baseMap (or extendMap override) based on options().options.type and renders it via NgComponentOutlet. Shows a skeleton placeholder while item() is undefined.

DetailBaseComponent (abstract)

Abstract base directive for all detail sub-components. Provides:

  • options: InputSignal<IDetailOptions<T>> — field configuration
  • cssClass: InputSignal<string> (alias class) — external CSS classes
  • afterSetOptionsHandler() — hook invoked via effect on options change
  • Effect that calls ChangeDetectorRef.detectChanges() when options change

Default Sub-Components

FieldTypeSub-componentSelectorDescription
textDetailTextComponentsmart-detail-textText via ListCellPipe + TrustHtmlPipe, fallback for unknown types
emailDetailEmailComponentsmart-detail-emailLink mailto:
enumDetailEnumComponentsmart-detail-enumSingle value or array with translate
flagDetailFlagComponentsmart-detail-flagInline SVG ✓ / ✗
colorDetailColorComponentsmart-detail-colorRectangle with background-color
addressDetailAddressComponentsmart-detail-addressIAddress (street, number, zip, city)
objectDetailObjectComponentsmart-detail-objectNested model via DETAILS_COMPONENT_TOKEN
arrayDetailArrayComponentsmart-detail-arrayArray of nested models
dateRangeDetailDateRangeComponentsmart-detail-date-rangestart – end
phoneNumberPlDetailPhoneNumberPlComponentsmart-detail-phone-number-plLink tel:48... as badge
imageDetailImageComponentsmart-detail-image<img> via FileService.getUrl(item[key].id)
logoDetailLogoComponentsmart-detail-logo<img> with item[key] as URL
videoDetailVideoComponentsmart-detail-video<video controls> via FileService
attachmentDetailAttachmentComponentsmart-detail-attachmentNative <button> Tailwind — download via FileService
pdfDetailPdfComponentsmart-detail-pdfNative <button> Tailwind — show via FileService

API

DetailComponent Inputs

InputTypeDefaultDescription
optionsInputSignal<IDetailOptions<T> | undefined>requiredField configuration
typeInputSignal<any>requiredModel class (used by ModelLabelPipe)

IDetailOptions

IFieldOptions (relevant properties): type?: FieldType, info?: string.

DETAIL_FIELD_COMPONENTS_TOKEN

InjectionToken<Partial<Record<FieldTypeDef, Type<DetailBaseComponent<any>>>>> — allows substituting a sub-component for any field type.

Maps are merged ({ ...baseMap, ...extendMap }), so only selected types need to be overridden.

Preline field presets

Fifteen field types ship a Preline-styled preset (Detail<Field>PresetComponent, selector smart-detail-<field>-preset) alongside the default sub-component, living in <field>/preset/:

FieldTypePresetLook
emailDetailEmailPresetComponentmailto: link, blue + hover:underline, envelope icon
enumDetailEnumPresetComponentValues as soft blue badges (reuses badge preset recipes)
flagDetailFlagPresetComponentSoft badge — green ✓ / red ✗ (reuses badge preset recipes)
colorDetailColorPresetComponentsize-6 rounded swatch + monospace hex code
addressDetailAddressPresetComponentMulti-line text-sm block with pin icon
dateRangeDetailDateRangePresetComponentstart – end as two soft gray chips
phoneNumberPlDetailPhoneNumberPlPresetComponenttel: link styled as a soft blue badge
logoDetailLogoPresetComponent<img> max-h-10 object-contain
imageDetailImagePresetComponent150×150 preview, rounded-xl, border + shadow-2xs
videoDetailVideoPresetComponent<video controls> framed rounded-xl border + shadow-2xs
attachmentDetailAttachmentPresetComponentFile chip: icon, file name (fileName/name), download
pdfDetailPdfPresetComponentFile chip: red PDF icon, file name, show button
textDetailTextPresetComponenttext-sm typography, text-pretty; em-dash when empty
objectDetailObjectPresetComponentCard envelope around the untouched nested details
arrayDetailArrayPresetComponentspace-y-2 stack of item cards; em-dash when empty

Apply them via the ready-made partial map:

Notes:

  • The map is partial — all other field types keep their standard components (maps merge over baseMap). Registering text in the map does NOT change the fallback for unmapped/unknown types — the map is keyed per FieldType, so those still fall back to the standard DetailTextComponent.
  • Unlike their standard components, the text and array presets render an em-dash placeholder when the value is empty (the standard components collapse to nothing).
  • The presets have a fixed look; IDetailOptions carries no per-field style channel (deliberate, matches the fidelity-gap deferral from the Preline group).

Extending the base class

import {
  ChangeDetectionStrategy,
  Component,
  computed,
  signal,
} from '@angular/core';

import {
  DetailBaseComponent,
  DetailComponent,
  DETAIL_FIELD_COMPONENTS_TOKEN,
  IDetailOptions,
} from '@smartsoft001/angular';
import { Field, FieldType, Model } from '@smartsoft001/models';

/**
 * A custom renderer for one detail field, built on `DetailBaseComponent`.
 *
 * The base supplies the `options` input (key, item signal, field metadata) and
 * the `class` input, and re-runs `afterSetOptionsHandler()` whenever the
 * options change. Everything else is the implementation's own template.
 */
@Component({
  selector: 'docs-custom-detail-text',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `<p [class]="classes()">{{ value() }}</p>`,
})
export class CustomDetailTextComponent extends DetailBaseComponent<
  Record<string, unknown>
> {
  value = computed(() => {
    const options = this.options();
    const item = options?.item?.();
    if (!options || !item) return '';
    return String(item[options.key] ?? '');
  });

  // `cssClass` is left aliased as `class` by the base — smart-detail forwards
  // the consumer's class under that name, so do not redeclare it here.
  classes = computed(() =>
    ['docs-detail-text', this.cssClass()].filter(Boolean).join(' '),
  );
}

@Model({})
class ApplicantModel {
  @Field({ details: true, type: FieldType.text })
  name = 'Margot Foster';
}

/**
 * `<smart-detail>` chooses the renderer from the field's `type`, so a custom
 * implementation is registered per FieldType through
 * `DETAIL_FIELD_COMPONENTS_TOKEN`. Entries are merged over the built-in map,
 * so only the listed types change.
 */
@Component({
  selector: 'docs-detail-custom-example',
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [DetailComponent],
  providers: [
    {
      provide: DETAIL_FIELD_COMPONENTS_TOKEN,
      useValue: { [FieldType.text]: CustomDetailTextComponent },
    },
  ],
  template: `
    <smart-detail
      [type]="type"
      [options]="options"
      [class]="'docs-detail-text--demo'"
    />
  `,
})
export class DetailCustomExampleComponent {
  type = ApplicantModel;

  options: IDetailOptions<ApplicantModel> = {
    key: 'name',
    options: { type: FieldType.text },
    item: signal(new ApplicantModel()),
  };
}

Source

The component lives in packages/shared/angular/src/lib/components/detail and is documented for Claude Code by the angular-components-detail skill.