npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

rtf-editor

v22.0.1

Published

This is an Angular Module containing Components/Services using Material

Readme

rtf-editor

A markdown-based rich text editor for Angular: a plain-textarea editor (RtfEditorComponent), a formatting toolbar (RtfEditorToolbarComponent), and a markdown-to-HTML preview renderer (RtfEditorPreviewComponent) that supports mounting live components — including externally-loaded custom elements — at named positions inside the rendered markdown.

Installation

npm install rtf-editor

Peer dependencies (must already be installed in the consuming app, same major version):

npm install @angular/core@^22 @angular/common@^22 @angular/material@^22 \
            @angular/platform-browser@^22 ngx-markdown@^22

ngx-markdown is a peer dependency, not a bundled one, and must be installed by the consuming app. This is not incidental: RtfEditorModule calls MarkdownModule.forChild(), which only works when the app has called MarkdownModule.forRoot() on the same ngx-markdown instance. Shipping it as an ordinary dependency allowed a second nested copy to be installed, whose module classes and DI tokens are distinct from the app's — producing a NullInjectorError or silently unconfigured rendering. Installing it yourself is what guarantees there is only one.

Note that ngx-markdown carries its own peer dependencies (marked, and optionally clipboard, emoji-toolkit, katex, mermaid, prismjs); your package manager will report any that your app needs and is missing.

marked is not required. rtf-editor imports only its types (import type { Tokens }), which are erased at compile time — the built bundle contains no marked import. If a future change needs marked at runtime, it must be declared as a peer alongside ngx-markdown, not as a dependency.

@angular/cdk is not required either — rtf-editor imports nothing from it. (@angular/material peers an exact @angular/cdk version of its own, so one will be present regardless.)

Import the module once, wherever you use any of these components:

import { RtfEditorModule } from 'rtf-editor';

@NgModule({
  imports: [RtfEditorModule],
  // ...
})
export class YourFeatureModule {}

RtfEditorModule declares and exports all four components below. It's an NgModule (not standalone) — every consuming app must be module-based, or import it into a module that a standalone app still bootstraps for this feature.

Components

RtfEditorComponent — <app-rtf-editor>

The editable surface (a styled <textarea>).

| Input | Type | Default | Description | |---|---|---|---| | content | string | a short built-in sample | Initial/controlled markdown source. | | editable | boolean | true | Set false for a read-only surface. | | height | string \| number | 'auto' | CSS height. | | minHeight | string \| number | — | CSS min-height. | | placeholder | string | — | Placeholder text when empty. | | autofocus | boolean | false | Focus on init. | | spellcheck | boolean | true | Native spellcheck attribute. | | maxLength | number | — | Enforced live — lowering it at runtime truncates existing content that now exceeds it, same as a native maxlength. | | ariaLabel | string | 'Markdown editor' | Accessible name for the surface. Defaulted so the textarea is never unnamed; override it with something specific to your form. Ignored when ariaLabelledby is set. | | ariaLabelledby | string | — | Id of your own visible label element. Prefer this over ariaLabel when a visible label exists — it takes precedence, so the two cannot disagree. | | ariaDescribedby | string | — | Id(s) of your own hint or error text, announced after the name. | | inputId | string | — | Sets the textarea's id, so your own <label for="…"> can target it. |

| Output | Payload | Fires | |---|---|---| | contentChange | string | On every edit. | | ready | HTMLTextAreaElement | Once, when the native element is available (e.g. to attach RtfEditorToolbarComponent or seed sample content programmatically). | | focus | void | On focus. | | blur | void | On blur. |

RtfEditorToolbarComponent — <app-rtf-editor-toolbar>

Formatting buttons (bold, headings, lists, etc.) that act directly on a textarea element you pass in.

| Input | Type | Default | Description | |---|---|---|---| | textarea | HTMLTextAreaElement | — | Typically RtfEditorComponent's (ready) payload. | | formatters | Partial<Record<RtfEditorToolbarAction, RtfEditorToolbarActionFn>> | {} | Override how a specific action mutates the textarea. | | hiddenActions | RtfEditorToolbarAction[] | [] | Hide specific buttons. |

RtfEditorPreviewComponent — <app-rtf-editor-preview>

Renders markdown to HTML and mounts live components/elements at slot:<name> positions (see Slots below).

| Input | Type | Default | Description | |---|---|---|---| | content | string | '' | Markdown source to render. | | tagClasses | Partial<Record<RtfEditorPreviewTag, string>> | {} | Override the CSS class this renderer applies to any element tag (headings, lists, code blocks, etc.), for theming without touching this library's own styles. | | slots | Record<string, Type<unknown> \| (() => HTMLElement \| Promise<HTMLElement>)> | {} | Maps a slot name to what gets mounted at that position. See Slots. |

RtfEditorDemoComponent — <app-rtf-editor-demo> (reference/demo only)

A working example wiring RtfEditorComponent + RtfEditorToolbarComponent + two side-by-side RtfEditorPreviewComponents together, including a sample slot of each kind described below. Not meant to be used as-is in a real app — read its source (projects/rtf-editor/src/lib/rtf-editor-demo/) as the canonical worked example for everything in this README.

| Input | Type | Default | Description | |---|---|---|---| | extraSlots | Record<string, Type<unknown> \| (() => HTMLElement \| Promise<HTMLElement>)> | {} | Merged on top of the demo's own built-in slots. Exists so a consuming app can register slots needing dependencies this library never takes on itself (e.g. Native Federation) — see External components below for why this matters. |

Slots

A markdown slot:<name> tag — either a fenced block on its own line:

```slot:myComponent
```

or written entirely on one line:

```slot:myComponent```

— gets replaced, once the surrounding markdown has rendered, with whatever slots()[name] (or extraSlots on the demo) resolves to:

  • An unregistered name renders nothing — not an error. This is intentional; a typo in a slot name shouldn't break the whole preview.
  • The same name used twice produces two independent instances — not a shared one.
  • A rejected/throwing resolution (see External components below) also renders nothing at that position, but does log a console.error, since that's a real runtime failure a developer needs to see, not an authoring typo.
  • Slot values receive no data from the markdown beyond their name and position. A slot owns its own inputs/outputs, API calls, and internal state entirely — RtfEditorPreviewComponent never passes anything into it.

There are exactly two shapes a slot value can take:

1. Internal components — a same-bundle Angular Type<unknown>

The simplest case — any Angular component already compiled into your app. Nothing extra to install.

import { Component } from '@angular/core';

@Component({
  selector: 'my-counter',
  template: `<button (click)="count = count + 1">{{ count }}</button>`,
})
export class MyCounterComponent {
  count = 0;
}
@Component({ /* ... */ })
export class MyPageComponent {
  slots = { myCounter: MyCounterComponent };
}
<app-rtf-editor-preview [content]="markdown" [slots]="slots"></app-rtf-editor-preview>

Mounted via ViewContainerRef.createComponent(componentType) — full change detection, DI, and lifecycle, exactly like any other dynamically-created Angular component.

2. External components — a function producing an HTMLElement

For anything not compiled into your app's own bundle — most commonly a custom element loaded at runtime via Native Federation from a separately-built, separately-deployed widget.

The slot value is a plain function:

type RtfEditorPreviewElementThunk = () => HTMLElement | Promise<HTMLElement>;

It's called once per render pass; its result (or resolved Promise) is inserted directly into the DOM at the slot's position via insertBefore/remove() — the same positional guarantee as the Type path. RtfEditorPreviewComponent never inspects the element beyond that: it may contain a whole separate, independent Angular application instance (as a federated widget typically does), and this library has no business reaching into it.

If a render pass starts again before an in-flight Promise resolves (e.g. the user is still typing), the stale result is discarded instead of being inserted into an already-removed placeholder — no manual bookkeeping needed on your end.

Full setup: loading a widget via Native Federation

This is the involved case, and the part most worth documenting in detail. Everything below lives in your consuming app, never inside rtf-editor itself — the whole point of the function-based slot shape is that this library stays completely unaware of Native Federation, or of how any particular element got built.

1. Install, in the consuming app (not in rtf-editor):

npm install @angular-architects/native-federation

This pulls in @softarc/native-federation, @softarc/native-federation-orchestrator, and es-module-shims transitively — no need to install those separately.

If the remote widget's exposed module is a raw Angular component class rather than an already-self-registering custom element (see step 6 — this is common, and easy to get wrong), also install:

npm install @angular/elements

2. federation.config.mjs, at your app's workspace root (next to angular.json):

import { withNativeFederation, shareAll, NG_SKIP_LIST } from '@angular-architects/native-federation/config';

export default withNativeFederation({
  name: 'your-app-name',
  shared: {
    ...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }),
  },
  skip: [
    ...NG_SKIP_LIST,
  ],
});

3. angular.json — your app's build/serve targets must use the Native Federation builder. This is not optional and not automatic just from federation.config.mjs existing — nothing reads that file unless the builder is wired in. It also isn't a simple in-place builder swap: the Native Federation builder wraps a separate, already-defined esbuild/dev-server target rather than replacing one outright. Rename your existing targets and add new ones delegating to them:

{
  "architect": {
    // Your ORIGINAL "build" target, renamed:
    "esbuild": {
      "builder": "@angular/build:application",
      "options": {
        // ... unchanged from before, except:
        "polyfills": ["zone.js", "es-module-shims"] // add this
      },
      "configurations": { /* unchanged */ }
    },
    // Your ORIGINAL "serve" target, renamed, buildTarget updated to "esbuild":
    "serve-original": {
      "builder": "@angular/build:dev-server",
      "options": { /* unchanged */ },
      "configurations": {
        "production": { "buildTarget": "your-app-name:esbuild:production" },
        "development": { "buildTarget": "your-app-name:esbuild:development" }
      },
      "defaultConfiguration": "development"
    },
    // NEW "build", delegating to "esbuild":
    "build": {
      "builder": "@angular-architects/native-federation:build",
      "options": { "cacheExternalArtifacts": true },
      "configurations": {
        "production": { "target": "your-app-name:esbuild:production" },
        "development": { "target": "your-app-name:esbuild:development", "dev": true }
      },
      "defaultConfiguration": "production"
    },
    // NEW "serve", delegating to "serve-original":
    "serve": {
      "builder": "@angular-architects/native-federation:build",
      "options": {
        "target": "your-app-name:serve-original:development",
        "rebuildDelay": 500,
        "cacheExternalArtifacts": true,
        "dev": true,
        "devServer": true,
        "port": 4300
      }
    }
  }
}

The es-module-shims polyfill is required — without it, the global importShim (which shim-mode import maps depend on) is never defined, and every federated module load fails immediately.

4. main.ts / bootstrap.ts split. Once your app's own build/serve run through the Native Federation builder, your app's own main.js is bundled as an es-module-shims-wrapped module that needs an import map for its own shared dependencies (@angular/core, @angular/platform-browser, etc.) just to boot — independent of whether you ever load an external widget. That import map only exists once initFederation() has run and fetched your app's own self-referential remoteEntry.json. A static import inside main.ts can't work here, no matter when you call initFederation() from within that same file's body: static imports resolve before a module's own code runs, so by the time any of your code executes, main.ts's own Angular imports have already failed. Split it instead:

// main.ts
import { initFederation } from '@angular-architects/native-federation';

initFederation({}) // no remotes here — see step 6
  .catch(err => console.error(err))
  .then(_ => import('./bootstrap')) // dynamic import: resolves AFTER the map exists
  .catch(err => console.error(err));
// bootstrap.ts — your app's actual, original main.ts content
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';

platformBrowserDynamic().bootstrapModule(AppModule)
  .catch(err => console.error(err));

(This is exactly what @angular-architects/native-federation's own ng add schematic generates automatically for freshly-scaffolded apps — do that instead of this by-hand split if you're setting up federation from scratch in a brand-new app.)

5. Where to put the widget's files. Copy the remote widget's own build output (e.g. dist/<widget>/browser/* from its own ng build) into your app's static assets, under a widgets/<widget-name>/ subfolder — e.g. src/assets/widgets/hello-widget/ (or public/widgets/hello-widget/ if your app uses Angular's newer public/ folder convention instead of src/assets/ — check your own angular.json's assets config to see which one actually applies). This serves the widget same-origin, so no second dev server is needed to test locally. Every widget your app loads this way should live under this same widgets/ folder — it's a convention, not something specific to any one widget.

6. The loader function itself. This is what actually becomes the slot's value. Two variants, depending on what the remote exposes:

If the remote's exposed module already self-registers a custom element (i.e. it calls customElements.define(...) as a side effect of its own bootstrap running):

import { loadRemoteModule } from '@angular-architects/native-federation';

export async function loadMyWidget(): Promise<HTMLElement> {
  await loadRemoteModule({
    remoteEntry: '/assets/widgets/my-widget/remoteEntry.json',
    exposedModule: './MyWidgetElement',
  });
  await customElements.whenDefined('my-widget');
  return document.createElement('my-widget');
}

If the remote's exposed module is a raw Angular component class instead (check by inspecting the served chunk — if it's just export { SomeComponent } with no customElements.define call anywhere, you're in this case; this is what ui-block-stack's hello-widget actually does, despite documentation suggesting otherwise). customElements.whenDefined would hang forever here, since nothing ever defines the tag. Wrap it yourself instead, via @angular/elements:

import { Injector, Type } from '@angular/core';
import { createCustomElement } from '@angular/elements';
import { loadRemoteModule } from '@angular-architects/native-federation';

let defined = false;

export async function loadMyWidget(injector: Injector): Promise<HTMLElement> {
  const module = await loadRemoteModule({
    remoteEntry: '/assets/widgets/my-widget/remoteEntry.json',
    exposedModule: './MyWidgetComponent',
  });
  const MyWidgetComponent = (module as { MyWidgetComponent: Type<unknown> }).MyWidgetComponent;
  if (!defined) {
    customElements.define('my-widget', createCustomElement(MyWidgetComponent, { injector }));
    defined = true;
  }
  await customElements.whenDefined('my-widget');
  return document.createElement('my-widget');
}

Note the top-level loadRemoteModule is used here (not the one returned by initFederation()'s own resolved result) — it's the only variant supporting the remoteEntry option, which is documented as enabling "lazy-loading remotes not registered during initFederation." That's deliberate: step 4's initFederation({}) call registers no remotes at all, so every widget gets registered lazily, on first actual use, the same way. The top-level function's deprecation note warns about relying on "the most recent initFederation call" in multi-host setups — irrelevant for a normal app that only ever calls initFederation() once.

If your widget's outputs surface as DOM CustomEvents (a common convention — check the widget's own docs), listen for them before returning the element:

element.addEventListener('someOutput', (event) => {
  console.log('someOutput:', (event as CustomEvent).detail);
});

7. Wire it into slots. Since this loader needs an Injector (only in the wrap-a-raw-component case) and imports @angular-architects/native-federation directly, keep it in your app's own source — never inside a shared, published library's component, even one that itself renders a RtfEditorPreviewComponent. A component like RtfEditorDemoComponent gets bundled as one shared file per published package (ng-packagr's FESM output); anything it imports becomes something every consumer of that package needs resolvable, whether or not they use the widget-loading feature at all.

// app.component.ts, or wherever you own the injection context
import { Injector, inject } from '@angular/core';
import { loadMyWidget } from './my-widget-slot';

export class AppComponent {
  private injector = inject(Injector);
  slots = { myWidget: () => loadMyWidget(this.injector) };
}
```slot:myWidget

For a complete, real, working reference implementation of every step above,
see `src/app/rtf-editor-hello-widget-slot.ts` and the `angular.json`/`main.ts`
changes in this workspace's own `app-lib` project (the app that runs
`RtfEditorDemoComponent`'s `hello-widget` slot).