---
title: "Actions & scripts — Angular"
description: "Decide what a PDF may do by itself: links, buttons, form calculations, JavaScript, opening websites and printing."
framework: "Angular"
source: "https://www.embedpdf.com/docs/headless/angular/documents/actions"
---

# Actions & scripts

A PDF can do things by itself: a button that resets a form, a field that adds up others, a link that
opens a website, a script that runs when the document opens. The actions plugin runs them, with
rules you choose. Add `withActions()` to `provideEmbedPdf()`, and `withActionsUi()` for the
browser's defaults for opening websites and printing:

**`actions-setup.ts`**

```typescript
import { Component } from '@angular/core';
import { provideEmbedPdf } from '@embedpdf/angular/runtime';
import { withActions, withActionsUi } from '@embedpdf/angular/actions';
import { engine } from './pdf';

@Component({
  selector: 'app-contract-viewer',
  providers: [provideEmbedPdf({ engine }, /* … */ withActions(), withActionsUi())],
  template: `<!-- your viewer -->`,
})
export class ContractViewer {}
```

Without it, the plugin still runs what stays inside the viewer, such as going to a page or resetting
a form, but it won't open a website or a print dialog.

## What runs

| Action                             | By default                                                          |
| :--------------------------------- | :------------------------------------------------------------------ |
| Go to a page or a destination      | Runs                                                                |
| A viewer action, such as next page | Runs                                                                |
| Show or hide a form field          | Runs                                                                |
| Reset a form                       | Runs                                                                |
| Open a website                     | Goes to your UI adapter when the user clicked; reported otherwise   |
| Print                              | Goes to your UI adapter when the user clicked; never on its own     |
| Submit a form                      | Goes to your submit handler when the user clicked; never on its own |
| JavaScript                         | Off, until you turn it on                                           |

Actions that start a program, open another file on the user's computer, play media or import data
never run.

## Turn on JavaScript

Many forms calculate totals, check values and format dates with JavaScript. Turn it on:

```ts
withActions({ javascript: { enabled: true } });
```

Scripts run in a sealed sandbox: they can read and change the document's form fields and move
between pages, but they can't reach your app, the page's HTML or the network. Each script has a
time budget, so a script that loops forever is stopped instead of freezing the viewer.

A script can ask who the user is (`identity.name` in Acrobat's JavaScript). It gets the user the
document was opened for, and you can add the fields scripts often ask for:

```ts
withActions({
  javascript: { enabled: true, identity: { corporation: 'Acme', email: 'dana@acme.com' } },
});
```

**`javascript.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  inject,
  Injectable,
  signal,
  ViewEncapsulation,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import {
  EpdfActions,
  withActions,
  withActionsUi,
  type ActionContext,
  type PdfActionTree,
} from '@embedpdf/angular/actions';
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()) };
};

// A JavaScript action, as a PDF carries it behind a button or a link.
const script = (code: string): PdfActionTree => ({
  root: { type: 'javascript', subtype: 'JavaScript', script: code, next: [] },
  incomplete: false,
  warningFlags: 0,
  warnings: [],
});
const click: ActionContext = {
  origin: 'user',
  source: { kind: 'api' },
  event: { scope: 'activate' },
};

const GREETING = "app.alert('Hello, ' + identity.name + ' from ' + identity.corporation + '!');";
const MISTAKE = 'app.alrt("typo");';

interface OutputLine {
  kind: 'alert' | 'error';
  text: string;
}

/** What the script said: its alerts and its errors, newest first. */
@Injectable()
export class ScriptOutput {
  readonly lines = signal<OutputLine[]>([]);

  show(kind: OutputLine['kind'], text: string) {
    this.lines.update((lines) => [{ kind, text }, ...lines].slice(0, 5));
  }
}

// Edit the script and run it. Its alerts and its errors show below it.
@Component({
  selector: 'demo-script-console',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <section class="panel">
      <label class="name" for="script">
        Script {{ actions.isScriptingEnabled() ? '' : '(JavaScript is off)' }}
      </label>
      <textarea
        #area
        id="script"
        class="code"
        spellcheck="false"
        rows="3"
        [value]="code()"
        (input)="code.set(area.value)"
      ></textarea>
      <div class="actions">
        <button type="button" class="button primary" (click)="run()">Run</button>
        <button type="button" class="button" (click)="code.set(mistake)">
          A script with a mistake
        </button>
      </div>
      <ol class="output" aria-live="polite">
        @for (line of output.lines(); track $index) {
          <li [class]="line.kind">{{ line.text }}</li>
        }
      </ol>
    </section>
  `,
})
export class ScriptConsole {
  protected readonly actions = inject(EpdfActions);
  protected readonly output = inject(ScriptOutput);
  protected readonly code = signal(GREETING);
  protected readonly mistake = MISTAKE;

  constructor() {
    this.actions.scriptFailed$
      .pipe(takeUntilDestroyed())
      .subscribe(({ error }) => this.output.show('error', error.message));

    // The greeting runs once on load.
    void this.actions.execute(script(GREETING), click);
  }

  protected run() {
    void this.actions.execute(script(this.code()), click);
  }
}

@Component({
  selector: 'demo-root',
  imports: [EpdfDocumentGate, ScriptConsole],
  providers: [
    ScriptOutput,
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      // JavaScript on, and two fields scripts often ask for about the user.
      withActions({
        javascript: { enabled: true, identity: { name: 'Dana Smith', corporation: 'Acme' } },
      }),
      // A script's alert goes to your UI adapter: here, a line below the script.
      withActionsUi(() => {
        const output = inject(ScriptOutput);
        return { alert: (message) => output.show('alert', message) };
      }),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './javascript.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <demo-script-console *epdfDocumentGate="let document; fallback: loading" />

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

**`javascript.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;
  --code: #f0f3f9;
  --danger: #c62a2a;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The example, in a card */
.panel {
  box-sizing: border-box;
  padding: 16px;
  background: var(--canvas);
  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);
}

.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 script */
.name {
  display: block;
  margin-bottom: 6px;
  font-size: 12px;
  font-weight: 600;
  color: var(--muted);
}

.code {
  box-sizing: border-box;
  width: 100%;
  resize: vertical;
  font-family: ui-monospace, monospace;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--ink);
  background: var(--code);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 10px 12px;
}

.code: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: 10px 0 12px;
}

/* What the script said, newest first */
.output {
  min-height: 40px;
  margin: 0;
  padding: 8px 12px;
  list-style: none;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 10px;
}

.output li {
  padding: 3px 0;
  font-size: 13px;
  overflow-wrap: anywhere;
}

.output .alert {
  color: var(--ink);
}

.output .error {
  font-family: ui-monospace, monospace;
  font-size: 12px;
  color: var(--danger);
}

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

## How websites open, printing and alerts

`withActionsUi()` installs the browser's defaults: a website opens in a new tab after a safety
check, printing opens the print dialog, and a script's alert shows the browser's alert. Replace any
of them with your own UI:

**`actions-ui.ts`**

```typescript
import { Component, inject } from '@angular/core';
import { provideEmbedPdf } from '@embedpdf/angular/runtime';
import { withActions, withActionsUi } from '@embedpdf/angular/actions';
import { engine } from './pdf';
import { Toasts } from './toasts';

@Component({
  selector: 'app-contract-viewer',
  providers: [
    provideEmbedPdf(
      { engine },
      /* … */
      withActions(),
      // Runs in an injection context, so your handlers can use your services
      withActionsUi(() => {
        const toasts = inject(Toasts);
        return {
          openUri: (uri) => {
            if (confirm(`Open ${uri}?`)) window.open(uri, '_blank', 'noopener');
          },
          alert: (message) => toasts.show(message),
        };
      }),
    ),
  ],
  template: `<!-- your viewer -->`,
})
export class ContractViewer {}
```

**`website-links.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  inject,
  Injectable,
  signal,
  ViewEncapsulation,
} from '@angular/core';
import { EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import {
  EpdfActions,
  withActions,
  withActionsUi,
  type ActionContext,
  type PdfActionTree,
} from '@embedpdf/angular/actions';
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()) };
};

// A link to a website, as a PDF carries it: what getActionTree() reads from a link.
const websiteLink: PdfActionTree = {
  root: { type: 'uri', subtype: 'URI', uri: 'https://www.embedpdf.com', isMap: false, next: [] },
  incomplete: false,
  warningFlags: 0,
  warnings: [],
};
const click: ActionContext = {
  origin: 'user',
  source: { kind: 'api' },
  event: { scope: 'activate' },
};

/** The website the document wants to open, while the user decides. */
@Injectable()
export class LinkPrompt {
  readonly asking = signal<string | null>(null);
}

// Your UI decides how a website opens: here, it asks first.
@Component({
  selector: 'demo-ask-before-opening',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <section class="panel">
      <p class="lead">The document links to <code>https://www.embedpdf.com</code>.</p>
      <button type="button" class="button" (click)="clickLink()">Click the link</button>
      @if (prompt.asking(); as asking) {
        <div class="prompt" role="alertdialog" aria-label="Open a website">
          <p class="prompt-text">
            This document wants to open <strong>{{ asking }}</strong
            >.
          </p>
          <div class="prompt-actions">
            <button type="button" class="button" (click)="prompt.asking.set(null)">
              Stay here
            </button>
            <button type="button" class="button primary" (click)="open(asking)">
              Open in a new tab
            </button>
          </div>
        </div>
      }
    </section>
  `,
})
export class AskBeforeOpening {
  private readonly actions = inject(EpdfActions);
  protected readonly prompt = inject(LinkPrompt);

  constructor() {
    // The link is clicked once on load, so the question is there to see.
    this.clickLink();
  }

  protected clickLink() {
    void this.actions.execute(websiteLink, click);
  }

  protected open(uri: string) {
    window.open(uri, '_blank', 'noopener');
    this.prompt.asking.set(null);
  }
}

@Component({
  selector: 'demo-root',
  imports: [EpdfDocumentGate, AskBeforeOpening],
  providers: [
    LinkPrompt,
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withActions(),
      withActionsUi(() => {
        const prompt = inject(LinkPrompt);
        return { openUri: (uri) => prompt.asking.set(uri) };
      }),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './website-links.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <demo-ask-before-opening *epdfDocumentGate="let document; fallback: loading" />

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

**`website-links.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);
}

/* The example, in a card */
.panel {
  box-sizing: border-box;
  padding: 16px;
  background: var(--canvas);
  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);
}

.lead {
  margin: 0 0 12px;
  font-size: 13.5px;
  color: var(--ink);
}

.lead code {
  font-size: 12.5px;
}

.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);
}

/* Your question, before a website opens */
.prompt {
  margin-top: 14px;
  padding: 14px 16px;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: 12px;
  box-shadow: 0 10px 30px -18px rgb(7 32 76 / 0.45);
}

.prompt-text {
  margin: 0 0 12px;
  font-size: 13.5px;
  color: var(--ink);
  overflow-wrap: anywhere;
}

.prompt-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 8px;
}

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

| Handler    | Called when                                   |
| :--------- | :-------------------------------------------- |
| `openUri`  | An action opens a website or an email address |
| `print`    | An action or a script asks to print           |
| `alert`    | A script shows a message                      |
| `gotoPage` | A script moves to a page                      |

## Change the rules

Each kind of action has a rule for each way it can start: a click by the user (`user`), the pointer
moving over something (`hover`), or the document opening or closing (`lifecycle`):

```ts
withActions({
  policy: {
    uri: { user: 'adapter', hover: 'block', lifecycle: 'block' }, // websites only on click
    javascript: { lifecycle: 'block' }, // no scripts when the document opens
  },
});
```

| Rule        | Means                                                                            |
| :---------- | :------------------------------------------------------------------------------- |
| `'allow'`   | Run it                                                                           |
| `'adapter'` | Hand it to your UI adapter (a form submit to your submit handler), which decides |
| `'report'`  | Don't run it; report it in `diagnosticReported$`                                 |
| `'block'`   | Don't run it                                                                     |

The rules can change while the app runs, for example when the user trusts a document:

```ts
actions.updateSettings({ policy: { javascript: { lifecycle: 'allow' } } });
```

**`rules.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  inject,
  Injectable,
  signal,
  ViewEncapsulation,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import {
  EpdfActions,
  withActions,
  withActionsUi,
  type ActionContext,
  type ActionOrigin,
  type ActionPolicyDecision,
  type PdfActionTree,
} from '@embedpdf/angular/actions';
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()) };
};

// A link to a website, as a PDF carries it: what getActionTree() reads from a link.
const websiteLink: PdfActionTree = {
  root: { type: 'uri', subtype: 'URI', uri: 'https://www.embedpdf.com', isMap: false, next: [] },
  incomplete: false,
  warningFlags: 0,
  warnings: [],
};

// The three ways an action can start, each as the context it runs with.
const STARTS: ReadonlyArray<{ label: string; context: ActionContext }> = [
  {
    label: 'A click',
    context: { origin: 'user', source: { kind: 'api' }, event: { scope: 'activate' } },
  },
  {
    label: 'The pointer over it',
    context: {
      origin: 'hover',
      source: { kind: 'api' },
      event: { scope: 'annotation', name: 'cursorEnter' },
    },
  },
  {
    label: 'The document opening',
    context: {
      origin: 'lifecycle',
      source: { kind: 'api' },
      event: { scope: 'document', name: 'open' },
    },
  },
];
const RULES: readonly ActionPolicyDecision[] = ['allow', 'adapter', 'report', 'block'];

/** What happened to the link, newest first. */
@Injectable()
export class RunLog {
  readonly lines = signal<string[]>([]);

  note(line: string) {
    this.lines.update((lines) => [line, ...lines].slice(0, 6));
  }
}

// The rules for websites, changed while the app runs, and what happens when the link runs.
@Component({
  selector: 'demo-website-rules',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <section class="panel">
      <ul class="starts">
        @for (start of starts; track start.context.origin) {
          <li class="start">
            <span class="start-label">
              {{ start.label }} <code>{{ start.context.origin }}</code>
            </span>
            <select
              #rule
              class="field"
              [attr.aria-label]="'The rule for ' + start.context.origin"
              (change)="setRule(start.context.origin, rule.value)"
            >
              @for (option of rulesToPick; track option) {
                <option [value]="option" [selected]="option === rules()[start.context.origin]">
                  {{ option }}
                </option>
              }
            </select>
            <button type="button" class="button" (click)="run(start.context)">Run the link</button>
          </li>
        }
      </ul>
      <div class="footer">
        <ol class="log" aria-live="polite">
          @for (line of log.lines(); track $index) {
            <li>{{ line }}</li>
          }
        </ol>
        <button type="button" class="button" (click)="actions.resetSettings()">
          Back to the registered rules
        </button>
      </div>
    </section>
  `,
})
export class WebsiteRules {
  protected readonly actions = inject(EpdfActions);
  protected readonly log = inject(RunLog);
  protected readonly starts = STARTS;
  protected readonly rulesToPick = RULES;
  protected readonly rules = computed(() => this.actions.settings().policy.uri);

  constructor() {
    this.actions.diagnosticReported$
      .pipe(takeUntilDestroyed())
      .subscribe(({ code, action }) => this.log.note(`The ${action} action was not run: ${code}`));

    // One click on load, so the log shows what a click does.
    this.run(STARTS[0].context);
  }

  protected run(context: ActionContext) {
    void this.actions.execute(websiteLink, context);
  }

  protected setRule(origin: ActionOrigin, rule: string) {
    this.actions.updateSettings({
      policy: { uri: { [origin]: rule as ActionPolicyDecision } },
    });
  }
}

@Component({
  selector: 'demo-root',
  imports: [EpdfDocumentGate, WebsiteRules],
  providers: [
    RunLog,
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      // Websites open only on a click, as the page's example registers it.
      withActions({
        policy: { uri: { user: 'adapter', hover: 'block', lifecycle: 'block' } },
      }),
      // Instead of opening a tab, the adapter notes the website it was given.
      withActionsUi(() => {
        const log = inject(RunLog);
        return { openUri: (uri) => log.note(`Your adapter got ${uri}`) };
      }),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './rules.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <demo-website-rules *epdfDocumentGate="let document; fallback: loading" />

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

**`rules.css`**

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

/* The example, in a card */
.panel {
  box-sizing: border-box;
  padding: 16px;
  background: var(--canvas);
  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);
}

.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;
}

/* One row per way to start: its rule and a button, wrapping on a phone */
.starts {
  margin: 0;
  padding: 0;
  list-style: none;
}

.start {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 10px;
  padding: 10px 0;
  border-bottom: 1px solid var(--line);
}

.start:first-child {
  padding-top: 0;
}

.start-label {
  flex: 1 1 180px;
  font-size: 13.5px;
  color: var(--ink);
}

.start-label code {
  margin-left: 4px;
  font-size: 12px;
  color: var(--muted);
}

.field {
  font: inherit;
  font-size: 13px;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 7px 10px;
}

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

/* What happened, newest first */
.footer {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: 10px;
  margin-top: 12px;
}

.log {
  flex: 1 1 240px;
  min-height: 64px;
  margin: 0;
  padding: 8px 12px;
  list-style: none;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 10px;
}

.log li {
  padding: 3px 0;
  font-size: 12.5px;
  color: var(--ink);
  overflow-wrap: anywhere;
}

.log li + li {
  color: var(--muted);
}

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

## Run an action from code

```ts
await actions.executeNamed('NextPage');
```

`executeNamed` runs a viewer action by its name: `'NextPage'`, `'PrevPage'`, `'FirstPage'`,
`'LastPage'` or `'Print'`. It follows the same rules as a click.

**`run-from-code.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  inject,
  Injectable,
  input,
  signal,
  ViewEncapsulation,
} from '@angular/core';
import { EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import {
  EpdfActions,
  withActions,
  withActionsUi,
  type PdfNamedAction,
} from '@embedpdf/angular/actions';
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 VERBS: ReadonlyArray<[PdfNamedAction, string]> = [
  ['FirstPage', '⇤ First'],
  ['PrevPage', '‹ Previous'],
  ['NextPage', 'Next ›'],
  ['LastPage', 'Last ⇥'],
  ['Print', 'Print'],
];

/** What the last action did. */
@Injectable()
export class LastRun {
  readonly text = signal<string | null>(null);
}

// The viewer actions a PDF's buttons name, run from your own buttons.
@Component({
  selector: 'demo-named-actions',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="toolbar">
      @for (verb of verbs; track verb[0]) {
        <button
          type="button"
          class="button"
          [disabled]="!actions.canExecuteNamed(verb[0])"
          (click)="run(verb[0])"
        >
          {{ verb[1] }}
        </button>
      }
      <span class="spacer"></span>
      <output class="readout">
        Page {{ stage().currentPageIndex() + 1 }} of {{ stage().pageCount() }}
        @if (ran.text(); as text) {
          <span class="ran"> · {{ text }}</span>
        }
      </output>
    </div>
  `,
})
export class NamedActions {
  readonly stage = input.required<EpdfStage>();
  protected readonly actions = inject(EpdfActions);
  protected readonly ran = inject(LastRun);
  protected readonly verbs = VERBS;

  constructor() {
    // On load, as if a "Last page" button in the PDF was clicked. Running it twice changes nothing.
    void this.actions
      .executeNamed('LastPage')
      .then(({ status }) => this.ran.text.set(`LastPage: ${status}`));
  }

  protected async run(name: PdfNamedAction) {
    const { status } = await this.actions.executeNamed(name);
    if (name !== 'Print') this.ran.text.set(`${name}: ${status}`);
  }
}

@Component({
  selector: 'demo-root',
  imports: [EpdfDocumentGate, EpdfStage, EpdfPageTemplate, EpdfRenderLayer, NamedActions],
  providers: [
    LastRun,
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: ebook }] },
      withStage(),
      withRender(),
      withActions(),
      // Print goes to your UI adapter. This one says so, instead of opening the browser's print dialog.
      withActionsUi(() => {
        const ran = inject(LastRun);
        return { print: () => ran.text.set('Print: your print handler ran') };
      }),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './run-from-code.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <demo-named-actions [stage]="stage" />
      <epdf-stage #stage="epdfStage" class="stage">
        <ng-template epdfPage>
          <epdf-render-layer />
        </ng-template>
      </epdf-stage>
    </ng-container>

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

**`run-from-code.css`**

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

/* The toolbar: it wraps on a phone */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.readout {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* Pushes what follows to the right */
.spacer {
  flex: 1 1 auto;
}

.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;
}

.ran {
  font-weight: 500;
  color: var(--muted);
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

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

## Methods

Everything `inject(EpdfActions)` gives you.

| Method                                                        | What it does                                                                                                                                                                                                                                          |
| :------------------------------------------------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `executeNamed(name, context?)`                                | Runs a viewer action by its name, such as `'NextPage'`, as if the user clicked it. [Run an action from code](#run-an-action-from-code)                                                                                                                |
| `execute(tree, context)`                                      | Runs actions you already have, such as a tree from `getActionTree()`, under the rules. Resolves `{ status, nodes, diagnostics }`: an action that fails is reported there, not thrown.                                                                 |
| `dispatch(trigger)`                                           | Reports that something happened, such as a click on a link or a page opening, and runs the actions behind it. The viewer does this for its own clicks and pages. Never rejects: problems come back in the result.                                     |
| `getActionTree(source)`                                       | Reads the actions behind a link or button, a form field, a page or the document, without running them. Resolves `null` when there are none.                                                                                                           |
| `isScriptingEnabled()`                                        | Whether this document's JavaScript runs: it's turned on and the script engine started. [Turn on JavaScript](#turn-on-javascript)                                                                                                                      |
| `setUiAdapter(adapter)`                                       | Installs your handlers for websites, printing and alerts in place of the ones before, and returns a function that removes them. `withActionsUi()` does this for you. [How websites open, printing and alerts](#how-websites-open-printing-and-alerts) |
| `setSubmitHandler(handler)`                                   | Sets the function a form's Submit button hands its values to, with the address the PDF names. The viewer never sends them anywhere itself. Returns a function that removes it.                                                                        |
| `registerExecutor(type, executor)`                            | Runs one kind of action, such as `'goto'`, with your own code in place of the built-in one. Returns a function that removes it.                                                                                                                       |
| `runDocumentVerb(verb, operation)`                            | Runs your own save (`'save'`) or print (`'print'`) between the document's before and after actions, and resolves with what `operation` returns. `download()` and `print()` do this by themselves.                                                     |
| `prepareClose()`                                              | Runs the document's closing actions. Call it before `documents.close()`, which skips them.                                                                                                                                                            |
| `getSettings()`, `updateSettings(changes)`, `resetSettings()` | Read the settings, change the rules while the app runs, and go back to what you registered. [Settings](#settings)                                                                                                                                     |
| `canExecute(tree, context)`, `canExecuteNamed(name)`          | Whether the rules would run these actions, or this viewer action. [Change the rules](#change-the-rules)                                                                                                                                               |
| `canDispatch(trigger)`                                        | Whether that kind of trigger is turned on in the `triggers` setting. [Settings](#settings)                                                                                                                                                            |

`context` is `{ origin, source, event }`: why the actions run (`'user'`, `'hover'` or `'lifecycle'`, as
in [Change the rules](#change-the-rules)), what started them, and which event it was. `executeNamed()`
fills it in as a click from your code. Every method that returns a promise also takes a `signal` in
its options, to cancel it.

## Settings

Set them when you register the plugin, and change them while the app runs with
`actions.updateSettings({ … })`; `actions.resetSettings()` goes back to what you registered, and
`inject(EpdfActions).settings()` gives you the current settings.

| Setting               | What it does                                                                                                                                                                                      | Default             |
| :-------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------------------ |
| `javascript.enabled`  | Run the document's JavaScript. A change applies to documents opened after it, since a document's script engine starts when it opens                                                               | `false`             |
| `javascript.identity` | Who scripts think the user is, on top of the document's user                                                                                                                                      | the document's user |
| `policy`              | The rules above, per kind of action and per origin                                                                                                                                                | as in the table     |
| `triggers`            | Run actions of the `document`, its `page`s, or `annotation`s                                                                                                                                      | all `true`          |
| `openSequence`        | When the document's opening actions run: `'auto'`, `'headless'` (as the document opens, for an app without a Stage), `'off'`. A change applies to documents whose opening actions haven't run yet | `'auto'`            |

## Events

**`action-diagnostics.ts`**

```typescript
import { Component, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { EpdfActions } from '@embedpdf/angular/actions';

@Component({ selector: 'app-action-diagnostics', template: '' })
export class ActionDiagnostics {
  constructor() {
    inject(EpdfActions)
      .diagnosticReported$.pipe(takeUntilDestroyed())
      .subscribe(({ code, action }) => console.warn(`Action ${action} was not run: ${code}`));
  }
}
```

| Event                       | Fires when                                                                                                            | Carries                    |
| :-------------------------- | :-------------------------------------------------------------------------------------------------------------------- | :------------------------- |
| `executed$`                 | Actions ran, whatever started them                                                                                    | `tree`, `result`, `source` |
| `diagnosticReported$`       | An action was blocked, reported or couldn't run                                                                       | `code`, `action`, `source` |
| `scriptFailed$`             | A script threw an error                                                                                               | `error`, `source`          |
| `scriptDiagnosticReported$` | A script was stopped from doing something, such as reaching the network, or used something the viewer doesn't support | `code`, `message`          |
| `openSequenceCompleted$`    | The document's opening actions ran                                                                                    | `result`                   |
| `settingsChanged$`          | A setting changed                                                                                                     | `settings`, `changed`      |

## Permissions

Actions don't need a permission of their own, but some things they do do: printing needs
`doc.print`, and changing a form field needs `doc.forms.fill`. An action the user may not perform
is reported, never run.

## Good to know

### When opening actions run

With `openSequence: 'auto'`, the actions a document runs when it opens wait for the first sign of a
person: the UI adapter being installed, or a first click. So a document that plays a script on open
doesn't do it in a background tab nobody has looked at.

### Scripts and forms

Form calculations, validation and formatting are scripts too: with JavaScript on, the
[form plugin](../forms) runs them as people type. See [Filling forms](../forms).
