---
title: "Metadata — Angular"
description: "Show and change a document's properties: title, author, subject, keywords, dates, and your own fields."
framework: "Angular"
source: "https://www.embedpdf.com/docs/headless/angular/documents/metadata"
---

# Metadata

A PDF carries information about itself: a title, an author, when it was made. Register
`withMetadata()` to show it in your UI and let people change it:

**`title-field.ts`**

```typescript
import { Component, computed, inject } from '@angular/core';
import { EpdfMetadata } from '@embedpdf/angular/metadata';

@Component({
  selector: 'app-title-field',
  template: `
    <label>
      Title
      <input
        #input
        [value]="title()"
        [disabled]="!metadata.canUpdate()"
        (blur)="metadata.update({ title: input.value })"
      />
    </label>
  `,
})
export class TitleField {
  protected readonly metadata = inject(EpdfMetadata);
  protected readonly title = computed(() => this.metadata.fields()?.title ?? '');
}
```

What `inject(EpdfMetadata)` gives you updates by itself: after your own changes, and on a shared
document, after someone else's. Nothing is read when a document opens: the metadata loads the
first time something asks for it, so a document nobody shows the properties of costs nothing.

## Read

`metadata.fields()` has every standard field. A field the file doesn't set is `null`:

| Field        | What it is                                                             |
| :----------- | :--------------------------------------------------------------------- |
| `title`      | The document's title                                                   |
| `author`     | Who wrote it                                                           |
| `subject`    | What it's about                                                        |
| `keywords`   | Keywords, as one string (often separated by commas)                    |
| `creator`    | The app the document was made in                                       |
| `producer`   | The app that turned it into a PDF                                      |
| `createdAt`  | When it was created, as an ISO date                                    |
| `modifiedAt` | When it was last changed, as an ISO date                               |
| `trapped`    | A print-shop setting: `'true'`, `'false'` or `'unknown'`, never `null` |

`status` says whether the metadata hasn't been asked for yet (`'idle'`), is loading
(`'loading'`), is there (`'ready'`), or is not allowed (`'forbidden'`). Reading the state is what
asks: `EpdfMetadata`
loads the metadata of the document it reads, so its first answer is `'loading'`. Show that while
it lasts, the way you show any other load:

```ts
const title = status === 'ready' ? (metadata?.title ?? 'Untitled') : 'Loading…';
```

From code, `ensureLoaded()` reads it and resolves once it's there:

```ts
await metadata.ensureLoaded();
const author = metadata.getSnapshot()?.author;
```

Every field `inject(EpdfMetadata)` gives you, and every status, is listed under [State](#state).

**`properties.ts`**

```typescript
import { ChangeDetectionStrategy, Component, inject, ViewEncapsulation } from '@angular/core';
import { EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { DocumentMetadata, OpenInput } from '@embedpdf/angular/runtime';
import { EpdfMetadata, withMetadata } from '@embedpdf/angular/metadata';
import { localEngine } from '@embedpdf/engine';

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

const FIELDS = [
  ['title', 'Title'],
  ['author', 'Author'],
  ['subject', 'Subject'],
  ['keywords', 'Keywords'],
  ['creator', 'Made in'],
  ['producer', 'Turned into a PDF by'],
  ['createdAt', 'Created'],
  ['modifiedAt', 'Last changed'],
  ['trapped', 'Trapped'],
] as const;

// Dates are ISO strings: show them in the reader's own format.
const DATES: ReadonlySet<string> = new Set(['createdAt', 'modifiedAt']);
const show = (field: string, value: string | null) => {
  if (value === null) return null;
  return DATES.has(field) ? new Date(value).toLocaleString() : value;
};

// The document's properties, the way a "Document properties" dialog shows them.
@Component({
  selector: 'demo-root',
  imports: [EpdfDocumentGate],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withMetadata(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './properties.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <section *epdfDocumentGate="let document; fallback: loading" class="panel">
      <header class="panel-header">
        <h3 class="panel-title">Document properties</h3>
        <span class="status" [attr.data-status]="metadata.status()">
          {{ metadata.status() }}
        </span>
      </header>
      <dl class="properties">
        @for (entry of fields; track entry[0]) {
          @let value = valueOf(entry[0], metadata.fields());
          <div class="property">
            <dt>{{ entry[1] }}</dt>
            <dd [class.unset]="value === null">{{ value ?? 'not set' }}</dd>
          </div>
        }
      </dl>
    </section>

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {
  protected readonly metadata = inject(EpdfMetadata);
  protected readonly fields = FIELDS;

  protected valueOf(field: (typeof FIELDS)[number][0], metadata: DocumentMetadata | null) {
    return metadata ? show(field, metadata[field]) : null;
  }
}
```

**`properties.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --surface: #ffffff;
  --canvas: #f7f9fc;
  --ready: #1f8a4c;
  --ready-tint: #e8f6ee;
  --waiting: #a86a00;
  --waiting-tint: #fdf3e1;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The properties, in a card */
.panel {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
  overflow: hidden;
}

.panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 16px;
  background: var(--canvas);
  border-bottom: 1px solid var(--line);
}

.panel-title {
  margin: 0;
  font-size: 14px;
  font-weight: 650;
  color: var(--ink);
}

/* The load state: loading, ready, forbidden or error */
.status {
  font-size: 11.5px;
  font-weight: 600;
  border-radius: 999px;
  padding: 3px 9px;
  color: var(--waiting);
  background: var(--waiting-tint);
}

.status[data-status='ready'] {
  color: var(--ready);
  background: var(--ready-tint);
}

/* One field per row: its name beside its value, stacked on a phone */
.properties {
  container-type: inline-size;
  margin: 0;
  padding: 4px 16px 10px;
}

.property {
  display: grid;
  grid-template-columns: 1fr;
  gap: 2px;
  padding: 9px 0;
  border-bottom: 1px solid var(--line);
}

.property:last-child {
  border-bottom: 0;
}

@container (min-width: 460px) {
  .property {
    grid-template-columns: 180px 1fr;
    gap: 16px;
  }
}

.property dt {
  font-size: 13px;
  color: var(--muted);
}

.property dd {
  margin: 0;
  font-size: 13px;
  font-weight: 550;
  color: var(--ink);
  overflow-wrap: anywhere;
}

.property dd.unset {
  font-weight: 400;
  font-style: italic;
  color: var(--muted);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

## Change

`update()` changes the fields you pass, and `null` removes one:

```ts
await metadata.update({
  title: 'Q2 Proposal (final)',
  author: 'Dana Smith',
  subject: null,
});
```

It resolves with `{ metadata }`: the fields after the change. The change is part of the document, kept when it's
downloaded.

**`edit.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  inject,
  input,
  OnInit,
  signal,
  ViewEncapsulation,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { DocumentMetadata, OpenInput } from '@embedpdf/angular/runtime';
import { EpdfMetadata, withMetadata } from '@embedpdf/angular/metadata';
import { localEngine } from '@embedpdf/engine';

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

const EDITABLE = ['title', 'author', 'subject', 'keywords'] as const;
type Draft = Record<(typeof EDITABLE)[number], string>;

// A properties form: the inputs start with what the file says, and Save writes them back.
@Component({
  selector: 'demo-properties-form',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="layout">
      <form class="form" (submit)="$event.preventDefault(); save()">
        @for (key of editable; track key) {
          <label class="row">
            <span class="name">{{ key }}</span>
            <input
              #field
              class="field"
              placeholder="not set"
              [value]="draft()[key]"
              [disabled]="!metadata.canUpdate()"
              (input)="edit(key, field.value)"
            />
          </label>
        }
        <div class="actions">
          <button type="submit" class="button primary" [disabled]="!metadata.canUpdate()">
            Save
          </button>
          <button
            type="button"
            class="button"
            [disabled]="!metadata.canUpdate()"
            (click)="modifiedNow()"
          >
            Set the modified date to now
          </button>
        </div>
      </form>
      <section class="changes" aria-live="polite">
        <h3 class="changes-title">Changes</h3>
        <p class="modified">Last changed: {{ modifiedAt() }}</p>
        @if (changes().length === 0) {
          <p class="empty">Edit a field and save.</p>
        } @else {
          <ul class="log">
            @for (change of changes(); track changes().length - $index) {
              <li>{{ change }}</li>
            }
          </ul>
        }
      </section>
    </div>
  `,
})
export class PropertiesForm implements OnInit {
  /** The fields as the file has them when the form starts. */
  readonly initial = input.required<DocumentMetadata>();
  protected readonly metadata = inject(EpdfMetadata);
  protected readonly editable = EDITABLE;
  protected readonly draft = signal<Draft>({ title: '', author: '', subject: '', keywords: '' });
  protected readonly changes = signal<string[]>([]);
  protected readonly modifiedAt = computed(() => {
    const modifiedAt = this.metadata.fields()?.modifiedAt ?? null;
    return modifiedAt ? new Date(modifiedAt).toLocaleString() : 'not set';
  });

  constructor() {
    // Every change to the standard fields, by you or anyone else.
    this.metadata.updated$
      .pipe(takeUntilDestroyed())
      .subscribe(({ changedKeys, origin }) =>
        this.changes.update((list) => [
          `${changedKeys.join(', ')} changed ${origin.kind === 'local' ? 'here' : 'elsewhere'}`,
          ...list,
        ]),
      );
  }

  ngOnInit() {
    const initial = this.initial();
    this.draft.set({
      title: initial.title ?? '',
      author: initial.author ?? '',
      subject: initial.subject ?? '',
      keywords: initial.keywords ?? '',
    });
  }

  protected edit(key: keyof Draft, value: string) {
    this.draft.update((draft) => ({ ...draft, [key]: value }));
  }

  // An empty field removes it: null, not ''.
  protected save() {
    const draft = this.draft();
    void this.metadata.update({
      title: draft.title.trim() || null,
      author: draft.author.trim() || null,
      subject: draft.subject.trim() || null,
      keywords: draft.keywords.trim() || null,
    });
  }

  protected modifiedNow() {
    void this.metadata.update({ modifiedAt: new Date() });
  }
}

// The form starts once the properties have been read from the file.
@Component({
  selector: 'demo-root',
  imports: [EpdfDocumentGate, PropertiesForm],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withMetadata(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './edit.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      @if (metadata.fields(); as fields) {
        <demo-properties-form [initial]="fields" />
      } @else {
        <p class="loading">Reading…</p>
      }
    </ng-container>

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {
  protected readonly metadata = inject(EpdfMetadata);
}
```

**`edit.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #0876fd;
  --accent-strong: #0565da;
  --on-accent: #ffffff;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

.button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 12px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.button.primary {
  color: var(--on-accent);
  background: var(--accent);
  border-color: var(--accent);
}

.button.primary:hover {
  background: var(--accent-strong);
  border-color: var(--accent-strong);
}

/* The form and the changes: side by side where there's room, stacked on a phone */
.layout {
  container-type: inline-size;
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}

.form,
.changes {
  flex: 1 1 100%;
  box-sizing: border-box;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 14px 16px;
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

@container (min-width: 620px) {
  .form {
    flex: 1 1 0;
    min-width: 0;
  }

  .changes {
    flex: 0 0 240px;
  }
}

/* One field per row: its name above its input */
.row {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-bottom: 10px;
}

.name {
  font-size: 12px;
  font-weight: 600;
  color: var(--muted);
}

.field {
  appearance: none;
  box-sizing: border-box;
  width: 100%;
  font: inherit;
  font-size: 13px;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 11px;
}

.field::placeholder {
  font-style: italic;
  color: var(--muted);
}

.field:focus-visible {
  outline: none;
  border-color: var(--focus);
  box-shadow: 0 0 0 3px rgb(125 182 255 / 0.35);
}

.actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 4px;
}

/* What changed, newest first */
.changes-title {
  margin: 0 0 6px;
  font-size: 13px;
  font-weight: 650;
  color: var(--ink);
}

.modified,
.empty {
  margin: 0 0 8px;
  font-size: 12.5px;
  color: var(--muted);
}

.log {
  margin: 0;
  padding: 0;
  list-style: none;
}

.log li {
  padding: 6px 0;
  font-size: 12.5px;
  color: var(--ink);
  border-top: 1px solid var(--line);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

## Your own fields

Keep your own values in the document, such as a contract number or who reviewed it. PDF apps show
them as custom properties:

**`custom-fields.ts`**

```typescript
import { Component, inject } from '@angular/core';
import { EpdfMetadata } from '@embedpdf/angular/metadata';

@Component({
  selector: 'app-contract-number',
  template: `<span>Contract {{ metadata.custom.fields()?.contractId }}</span>`,
})
export class ContractNumber {
  // metadata.custom.fields() is { contractId: 'C-2026-114', reviewedBy: 'dana' }
  protected readonly metadata = inject(EpdfMetadata);
}
```

```ts
await metadata.custom.update({ reviewedBy: 'lee', draftOwner: null });
```

A string sets a field, `null` removes it, and fields you leave out stay as they are. A field name is
1 to 127 plain characters and can't be a standard field's name.

**`custom-fields.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  inject,
  signal,
  ViewEncapsulation,
} from '@angular/core';
import { EpdfDocumentGate, isPluginError, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfMetadata, withMetadata } from '@embedpdf/angular/metadata';
import { localEngine } from '@embedpdf/engine';

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};

// Your own fields, kept in the document: add one, change one, remove one.
@Component({
  selector: 'demo-custom-fields',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <section class="panel">
      <table class="fields">
        <thead>
          <tr>
            <th>Name</th>
            <th>Value</th>
            <th aria-label="Remove"></th>
          </tr>
        </thead>
        <tbody>
          @for (field of fields(); track field[0]) {
            <tr>
              <td class="key">{{ field[0] }}</td>
              <td>{{ field[1] }}</td>
              <td>
                <!-- null removes a field; the others stay as they are. -->
                <button
                  type="button"
                  class="button quiet"
                  [disabled]="!metadata.canUpdate()"
                  (click)="remove(field[0])"
                >
                  Remove
                </button>
              </td>
            </tr>
          }
        </tbody>
      </table>
      <form class="add" (submit)="$event.preventDefault(); add()">
        <input
          #nameField
          class="field"
          aria-label="Name"
          placeholder="Name, such as approvedOn"
          [value]="name()"
          (input)="name.set(nameField.value)"
        />
        <input
          #valueField
          class="field"
          aria-label="Value"
          placeholder="Value"
          [value]="value()"
          (input)="value.set(valueField.value)"
        />
        <button type="submit" class="button" [disabled]="!metadata.canUpdate() || !name().trim()">
          Add
        </button>
      </form>
      @if (error(); as error) {
        <p class="error">{{ error }}</p>
      }
    </section>
  `,
})
export class CustomFields {
  protected readonly metadata = inject(EpdfMetadata);
  protected readonly fields = computed(() => Object.entries(this.metadata.custom.fields() ?? {}));
  protected readonly name = signal('');
  protected readonly value = signal('');
  protected readonly error = signal<string | null>(null);

  constructor() {
    // Two fields of our own, written on load. Setting the same values again changes nothing.
    void this.metadata.custom.update({ contractId: 'C-2026-114', reviewedBy: 'dana' });
  }

  protected async add() {
    try {
      await this.metadata.custom.update({ [this.name().trim()]: this.value() });
      this.name.set('');
      this.value.set('');
      this.error.set(null);
    } catch (failure) {
      // A name the PDF can't take, such as one of the standard fields.
      if (isPluginError(failure, 'invalid-input')) this.error.set(failure.message);
      else throw failure;
    }
  }

  protected remove(key: string) {
    void this.metadata.custom.update({ [key]: null });
  }
}

@Component({
  selector: 'demo-root',
  imports: [EpdfDocumentGate, CustomFields],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withMetadata(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './custom-fields.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <demo-custom-fields *epdfDocumentGate="let document; fallback: loading" />

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {}
```

**`custom-fields.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --danger: #c62a2a;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The fields, in a card */
.panel {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
  overflow: hidden;
}

.fields {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}

.fields th {
  padding: 10px 16px;
  font-size: 12px;
  font-weight: 600;
  text-align: left;
  color: var(--muted);
  background: var(--canvas);
  border-bottom: 1px solid var(--line);
}

.fields td {
  padding: 8px 16px;
  color: var(--ink);
  border-bottom: 1px solid var(--line);
  overflow-wrap: anywhere;
}

.fields td:last-child {
  width: 1%;
  text-align: right;
}

.key {
  font-family: ui-monospace, monospace;
  font-size: 12.5px;
}

/* A new field: name, value and the button, wrapping on a phone */
.add {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding: 12px 16px;
}

.field {
  appearance: none;
  flex: 1 1 150px;
  min-width: 0;
  box-sizing: border-box;
  font: inherit;
  font-size: 13px;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 11px;
}

.field::placeholder {
  color: var(--muted);
}

.field:focus-visible {
  outline: none;
  border-color: var(--focus);
  box-shadow: 0 0 0 3px rgb(125 182 255 / 0.35);
}

.button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 14px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.button.quiet {
  padding: 6px 10px;
  font-size: 12px;
  color: var(--danger);
  box-shadow: none;
}

.error {
  margin: 0;
  padding: 0 16px 12px;
  font-size: 12.5px;
  color: var(--danger);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

## Methods

Everything `inject(EpdfMetadata)` gives you.

| Method                          | What it does                                                                                                                             |
| :------------------------------ | :--------------------------------------------------------------------------------------------------------------------------------------- |
| `update(changes)`               | Changes the standard fields you pass; `null` removes one. Resolves `{ metadata }`, the fields after the change. [Change](#change)        |
| `custom.update(changes)`        | Changes your own fields: a string sets one, `null` removes it, the rest stay. Resolves `{ custom }`. [Your own fields](#your-own-fields) |
| `ensureLoaded()`                | Reads the standard fields and your own from the file, unless they're loaded or loading. Resolves once they're there. [Read](#read)       |
| `refresh()`, `custom.refresh()` | Reads the standard fields, or your own, from the file again. Rarely needed: they stay up to date by themselves.                          |
| `canUpdate()`                   | Whether changing the standard fields and your own ones is allowed. [Permissions](#permissions)                                           |

Every method that returns a promise also takes a `signal` in its options, to cancel it.

## State

Every value is a signal on `inject(EpdfMetadata)`: read it in a template or a `computed()`, and your view updates when it changes. The method in the last column reads the same value once.

| Field             | What it is                                                                                    | Read once                                                  |
| :---------------- | :-------------------------------------------------------------------------------------------- | :--------------------------------------------------------- |
| `fields()`        | Every standard field, as listed under [Read](#read), or `null` until it has loaded            | `getSnapshot()`                                            |
| `custom.fields()` | Your own fields, as `{ name: value }`, or `null` until they have loaded                       | `custom.getSnapshot()`                                     |
| `status()`        | `'idle'` until something asks for it, then `'loading'`, `'ready'`, `'forbidden'` or `'error'` | `getStatus()`, or `custom.getStatus()` for your own fields |

## Events

**`title-notice.ts`**

```typescript
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { filter, map } from 'rxjs';
import { EpdfMetadata } from '@embedpdf/angular/metadata';

@Component({
  selector: 'app-title-notice',
  template: `<p role="status">{{ notice() }}</p>`,
})
export class TitleNotice {
  private readonly metadata = inject(EpdfMetadata);

  protected readonly notice = toSignal(
    this.metadata.updated$.pipe(
      filter(({ origin }) => origin.kind === 'remote'),
      map(({ metadata }) => `The title is now "${metadata.title}"`),
    ),
    { initialValue: '' },
  );
}
```

| Event              | Fires when                                                                                                                                 | Carries                                         |
| :----------------- | :----------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------- |
| `updated$`         | The standard fields changed, by you or by someone else                                                                                     | `metadata`, `previous`, `changedKeys`, `origin` |
| `resynced$`        | The standard fields were loaded from the file: the first time they're asked for, after `refresh()`, or to catch up after a lost connection | `metadata`                                      |
| `custom.updated$`  | Your own fields changed                                                                                                                    | `custom`, `previous`, `changedKeys`, `origin`   |
| `custom.resynced$` | Your own fields were loaded from the file                                                                                                  | `custom`                                        |

`changedKeys` names the fields that changed, and `previous` has the values before.

## Permissions

| Permission            | What it allows                                 | Check                  |
| :-------------------- | :--------------------------------------------- | :--------------------- |
| `doc.open`            | Reading every field                            | none: it opened        |
| `doc.metadata.modify` | Changing the standard fields and your own ones | `metadata.canUpdate()` |

## Good to know

### The modified date

Changing a property doesn't change `modifiedAt` for you. Set it yourself when you want it to move:
`update({ modifiedAt: new Date() })`.

### Dates

Dates are ISO strings, with the time zone the PDF gives, so they survive JSON. Write a date as an
ISO string or a `Date`. See [Metadata](https://www.embedpdf.com/docs/engine/documents/metadata) for the details.
