# @yuuvis/client-framework/renderer

Secondary entry point of `@yuuvis/client-framework`. Import everything from `@yuuvis/client-framework/renderer`.

Provides a pluggable rendering layer for two distinct things:

1. **Property renderers** — components that render a single object-type property value (string, integer, datetime, file size, organization, table, …) inside list rows, summary panels, tile slots, smart-search results, and detail headers.
2. **Audit renderers** — components that render the inner content of a single entry in the audit timeline (`ObjectAuditComponent`), keyed by the audit `action` (and optionally `subaction`).

Both subsystems share the same shape: a service that maintains a registry, an abstract base component that consumers extend, and a directive that resolves and instantiates the right component for a given input.

## Public API

| Export | Kind | Purpose |
|---|---|---|
| `RendererService` | service (`providedIn: 'root'`) | Registry for property renderers. |
| `RendererDirective` (`*yuvRenderer`) | structural directive | Renders a `ResolvedObjectConfigItem` using the registered renderer. |
| `AbstractRendererComponent<T, U>` | abstract component | Base class for custom property renderers. |
| `RendererComponent` | type | Union of the built-in property renderer components. |
| `RendererDirectiveInput` | type alias of `ResolvedObjectConfigItem` | Input shape for `*yuvRenderer`. |
| `AuditRendererService` | service (`providedIn: 'root'`) | Registry for audit-entry renderers. |
| `AuditRendererDirective` (`[yuvAuditRenderer]`) | attribute directive | Renders an `AuditEntry` using the registered renderer. |
| `AbstractAuditRendererComponent` | abstract component | Base class for custom audit renderers. |
| `DefaultAuditRendererComponent` | component | Built-in fallback that resolves localized audit labels for all known actions (100, 200, 300, 400, custom 10000). |
| Built-in property renderers | components | `StringRendererComponent`, `IntegerRendererComponent`, `DecimalRendererComponent`, `BooleanRendererComponent`, `DateTimeRendererComponent`, `FileSizeRendererComponent`, `IconRendererComponent`, `OrganizationRendererComponent`, `TableRendererComponent`, `UnknownRendererComponent` (fallback). |

---

## Property renderers

### Rendering a property — `*yuvRenderer`

`*yuvRenderer` takes a `ResolvedObjectConfigItem` and instantiates the matching renderer into the host view. The host owns the layout; the directive owns the value cell.

```html
@for (property of object.properties; track property.propertyName) {
  <div class="row">
    <div class="label">{{ property.label }}</div>
    <div class="value">
      <ng-container *yuvRenderer="property.value" />
    </div>
  </div>
}
```

Input shape:

```ts
interface ResolvedObjectConfigItem {
  propertyName: string;              // schema property name (e.g. 'system:contentStreamLength')
  rendererType?: RendererType;       // optional explicit renderer key — wins over propertyName lookup
  value?: unknown;                   // the value passed to the renderer
  meta?: Record<string, unknown>;    // additional context (e.g. *_title for user fields)
}
```

Resolution order inside `RendererService`:

1. If `rendererType` is set → look up by that key (`getRendererByType`).
2. Otherwise look up by `propertyName` (`getRenderer`). If not registered, fall back to the property's internal field type derived from `SystemService`.
3. If nothing matches → `UnknownRendererComponent`.

The directive re-renders only when the input object actually changes (deep equality via `JSON.stringify`).

### Built-in registrations

`RendererService` ships with these defaults (in `RendererService` constructor):

| Key | Component |
|---|---|
| `integer` | `IntegerRendererComponent` |
| `decimal` | `DecimalRendererComponent` |
| `datetime` | `DateTimeRendererComponent` |
| `icon` | `IconRendererComponent` |
| `string` | `StringRendererComponent` |
| `boolean` | `BooleanRendererComponent` |
| `table` | `TableRendererComponent` |
| `InternalFieldType.STRING_ORGANIZATION` / `STRING_ORGANIZATION_SET` | `OrganizationRendererComponent` |
| `ContentStreamField.LENGTH` | `FileSizeRendererComponent` |

### Registering a custom renderer

Get the service and call one of the registration methods. Either overrides the default for that key.

```ts
import { inject, Injectable } from '@angular/core';
import { RendererService } from '@yuuvis/client-framework/renderer';
import { MyHashRendererComponent } from './my-hash.renderer';

@Injectable({ providedIn: 'root' })
export class MyAppRendererSetup {
  #renderers = inject(RendererService);

  init(): void {
    // Override by renderer type (matches `rendererType` on ResolvedObjectConfigItem)
    this.#renderers.registerRendererByType('string', MyHashRendererComponent);

    // Or scope to a single property — and optionally to a specific object type
    this.#renderers.registerRenderer(MyHashRendererComponent, 'document:sha256');
    this.#renderers.registerRenderer(MyHashRendererComponent, 'document:sha256', 'system:document');
  }
}
```

Call this from `APP_INITIALIZER` or any service that runs before the views using the directive are constructed. Registrations are stored in a signal, so consumers that re-resolve their renderer will pick up changes; however the directive only re-resolves when its input changes — switching renderers at runtime requires re-pushing the input.

### Writing a custom property renderer

Extend `AbstractRendererComponent<T, U>`. `T` is the value type, `U` is the meta-shape (defaults to `null`).

```ts
import { ChangeDetectionStrategy, Component, computed } from '@angular/core';
import { AbstractRendererComponent } from '@yuuvis/client-framework/renderer';

@Component({
  selector: 'app-hash-renderer',
  standalone: true,
  template: `<code class="hash" [title]="value()">{{ short() }}</code>`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class MyHashRendererComponent extends AbstractRendererComponent<string> {
  protected short = computed(() => this.value()?.slice(0, 12) ?? '');
}
```

The base class exposes:

- `propertyName = input.required<string>()`
- `value = input.required<T | null>()`
- `meta = input<Record<string, unknown> | U>()`
- `getProperty(): SchemaResponseFieldDefinition | undefined` — schema definition for the property from `SystemService`.

All renderers must be `standalone: true` and should use `ChangeDetectionStrategy.OnPush`.

---

## Audit renderers

### Rendering an audit entry — `[yuvAuditRenderer]`

`[yuvAuditRenderer]` is the attribute-directive twin for the audit timeline. The host component (`ObjectAuditComponent`) owns the date column, timeline line, version badge, and creator label; the renderer owns the *inner content* of the entry.

```html
@for (item of items(); track item.creationDate + '-' + item.action) {
  <li class="audit">
    <time>{{ item.creationDate | localeDate }}</time>
    <div class="content"><ng-container [yuvAuditRenderer]="item" /></div>
    <span class="version">v{{ item.version }}</span>
    <div class="creator">{{ item.createdBy.title }}</div>
  </li>
}
```

Resolution order inside `AuditRendererService.getAuditRenderer(action, subaction)`:

1. `(action, subaction)` — if `subaction` is provided and a renderer is registered for the exact pair.
2. `(action)` — action-only registration.
3. `DefaultAuditRendererComponent` — built-in fallback that translates the well-known audit labels (`yuv.audit.label.*`) for actions 100/200/300/400 and custom action 10000.

### Registering a custom audit renderer

```ts
import { inject, Injectable } from '@angular/core';
import { AuditRendererService } from '@yuuvis/client-framework/renderer';
import { CreatedAuditRendererComponent } from './created-audit.renderer';
import { UpdatedAuditRendererComponent } from './updated-audit.renderer';

@Injectable({ providedIn: 'root' })
export class MyAppAuditSetup {
  #audit = inject(AuditRendererService);

  init(): void {
    // Override every CREATE_METADATA (action 100) entry
    this.#audit.registerAuditRenderer(CreatedAuditRendererComponent, 100);

    // Override one specific (action, subaction) pair
    this.#audit.registerAuditRenderer(UpdatedAuditRendererComponent, 300, 42);
  }
}
```

### Writing a custom audit renderer

Extend `AbstractAuditRendererComponent`. The directive sets one input: `auditEntry: AuditEntry`.

```ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { AbstractAuditRendererComponent } from '@yuuvis/client-framework/renderer';

@Component({
  selector: 'app-created-audit',
  standalone: true,
  template: `
    <span class="title">📄 Document created</span>
    <small class="meta">action {{ auditEntry().action }}</small>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class CreatedAuditRendererComponent extends AbstractAuditRendererComponent {}
```

The base class exposes:

- `auditEntry = input.required<AuditEntry>()` — the full entry, including `action`, `subaction`, `detail`, `creationDate`, `createdBy`, `version`.

If you want to extend (rather than replace) the default labelling, you can compose `DefaultAuditRendererComponent` inside your own template, or read `auditEntry().detail` directly — its format is action-specific (the default renderer parses `[a,b]`-style payloads for tag/restore actions).

---

## Conventions

- Both directives, both services, and both abstract base classes are tree-shakable per sub-library — import only what you need.
- Renderers are looked up at directive instantiation time. Registering after the view exists is fine but only affects later input changes.
- All built-in renderers use OnPush and are standalone — follow the same in custom renderers.
- `RendererService` integrates with `SystemService` to fall back to the property's internal field type; custom renderers do not need to replicate this — register against the field-type key (`registerRendererByType`) to opt in.
