Skip to content

Latest commit

 

History

History

angular

README.md

@markitjs/angular

Angular bindings for the @markitjs/core text highlighting engine. Provides a standalone MarkitHighlightDirective and an injectable MarkitService.

Install

npm install @markitjs/angular
# or
bun add @markitjs/angular
# or
pnpm add @markitjs/angular
# or (Yarn — include @markitjs/core explicitly)
yarn add @markitjs/angular @markitjs/core

Peer dependencies: Angular 17, 18, or 19, and @markitjs/core (installed automatically by npm 7+, pnpm, and bun; with Yarn, add @markitjs/core explicitly: yarn add @markitjs/angular @markitjs/core).

Quick Start

Directive

Import the standalone directive in your component:

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

@Component({
  selector: 'app-search',
  standalone: true,
  imports: [MarkitHighlightDirective],
  template: `
    <input [(ngModel)]="searchTerm" placeholder="Search..." />

    <div [markitHighlight]="searchTerm" [markitOptions]="highlightOptions">
      <p>This content will be searched and highlighted.</p>
      <app-child></app-child>
      <!-- bindings preserved -->
    </div>
  `,
})
export class SearchComponent {
  searchTerm = '';
  highlightOptions = { caseSensitive: false, accuracy: 'partially' as const };
}

Service

For programmatic control, inject MarkitService:

import { Component, ElementRef, OnDestroy, ViewChild } from '@angular/core';
import { MarkitService } from '@markitjs/angular';
import type { MarkitInstance } from '@markitjs/core';

@Component({
  selector: 'app-editor',
  template: `<div #content>Searchable content here.</div>`,
})
export class EditorComponent implements OnDestroy {
  @ViewChild('content', { static: true }) contentRef!: ElementRef<HTMLElement>;

  private instance: MarkitInstance | null = null;

  constructor(private markitService: MarkitService) {}

  search(term: string) {
    this.instance?.destroy();
    this.instance = this.markitService.create(this.contentRef.nativeElement);
    this.markitService.highlight(this.instance, term, {
      renderer: 'dom',
      caseSensitive: false,
    });
  }

  clear() {
    this.markitService.clear(this.instance!);
  }

  ngOnDestroy() {
    this.instance?.destroy();
  }
}

API

MarkitHighlightDirective

Standalone attribute directive. Apply to any element whose text content you want to highlight.

Input Type Description
markitHighlight string | string[] Search term(s) to highlight
markitOptions Partial<MarkitOptions> All @markitjs/core options (renderer, accuracy, etc.)
markitPlugins MarkitPlugin[] Plugins to register
markitContentKey string | number | (string | number)[] When content is dynamic, pass value(s) that change with content so the directive unmarks and re-applies.

For dynamic content (e.g. bound to a signal), pass [markitContentKey] so highlights re-apply after content updates and avoid garbled text. See Framework lifecycles for how the Angular highlight cycle works.

MarkitService

Injectable service (providedIn: 'root'). All operations run outside NgZone.

Method Description
create(element, plugins?) Create a MarkitInstance
highlight(instance, term, options?) Apply keyword highlighting
highlightRegExp(instance, regexp, options?) Apply regex highlighting
clear(instance) Remove all highlights

NgZone Safety

Both the directive and service run highlighting outside NgZone to avoid triggering unnecessary change detection cycles. This is especially important for DOM-mutating renderers.

Callbacks (done, noMatch) automatically re-enter the zone, so updating component state from callbacks works correctly:

highlightOptions = {
  done: (count: number) => {
    this.matchCount = count; // safe — re-enters NgZone automatically
  },
};

Change Detection Compatibility

Strategy Works? Notes
Default Yes No unnecessary cycles triggered
OnPush Yes Changes fire via @Input bindings through OnChanges
Signals Yes Bind a signal in the template; directive reacts via inputs
Zoneless Yes Operations already run outside zone

Preserving Bindings

Unlike innerHTML-based approaches, MarkIt never replaces DOM nodes. The CSS Highlight API (default) creates zero DOM mutations. The DOM wrapping renderer splits text nodes and wraps matches while keeping the original text node in place—including when the match is at the start of the text—so framework bindings (e.g. {{ value }}) continue to update correctly and it never destroys component instances, event listeners, or form control state.

Batched Rendering

For large content areas, enable batched rendering:

<div [markitHighlight]="searchTerm" [markitOptions]="{ batchSize: 500, done: onHighlightDone }">
  <!-- large content -->
</div>

Performance tip: For large documents or fast typing (e.g. live search), pass debounce and/or batchSize in markitOptions. debounce (ms) reduces re-index/render cycles when the term changes quickly; batchSize splits rendering across animation frames so the UI stays responsive. Both are supported by the core engine and work with the directive and service.

CSS Highlight API Styling

When using the default auto renderer on supported browsers, add this to your global styles:

::highlight(markit-highlight) {
  background-color: #fef08a;
  color: inherit;
}

License

MIT