diff --git a/angular-workspace/projects/example-client-app/src/app/app.module.ts b/angular-workspace/projects/example-client-app/src/app/app.module.ts
index 45721b321b..b47de93b03 100644
--- a/angular-workspace/projects/example-client-app/src/app/app.module.ts
+++ b/angular-workspace/projects/example-client-app/src/app/app.module.ts
@@ -22,6 +22,7 @@ import { NimbleTableColumnDateTextModule } from '@ni/nimble-angular/table-column
import { NimbleTableColumnEnumTextModule } from '@ni/nimble-angular/table-column/enum-text';
import { NimbleTableColumnIconModule } from '@ni/nimble-angular/table-column/icon';
import { NimbleRichTextViewerModule } from '@ni/nimble-angular/rich-text-viewer';
+import { NimbleRichTextEditorModule } from '@ni/nimble-angular/rich-text-editor';
import { AppComponent } from './app.component';
import { CustomAppComponent } from './customapp/customapp.component';
import { HeaderComponent } from './header/header.component';
@@ -84,6 +85,7 @@ import { HeaderComponent } from './header/header.component';
NimbleMappingTextModule,
NimbleBannerModule,
NimbleRichTextViewerModule,
+ NimbleRichTextEditorModule,
NimbleTableColumnIconModule,
NimbleMappingIconModule,
NimbleMappingSpinnerModule,
diff --git a/angular-workspace/projects/example-client-app/src/app/customapp/customapp.component.html b/angular-workspace/projects/example-client-app/src/app/customapp/customapp.component.html
index 8f42865860..cd00a06c35 100644
--- a/angular-workspace/projects/example-client-app/src/app/customapp/customapp.component.html
+++ b/angular-workspace/projects/example-client-app/src/app/customapp/customapp.component.html
@@ -170,10 +170,18 @@
{{ item ? item.first : '' }}
+
+
Rich Text Editor
+
+
+ Load Content
+
+
+
diff --git a/angular-workspace/projects/example-client-app/src/app/customapp/customapp.component.scss b/angular-workspace/projects/example-client-app/src/app/customapp/customapp.component.scss
index def466ea8f..065d6d542b 100644
--- a/angular-workspace/projects/example-client-app/src/app/customapp/customapp.component.scss
+++ b/angular-workspace/projects/example-client-app/src/app/customapp/customapp.component.scss
@@ -83,3 +83,7 @@ nimble-table {
padding: $ni-nimble-small-padding;
width: 350px;
}
+
+.rich-text-editor-container {
+ padding: $ni-nimble-small-padding;
+}
diff --git a/angular-workspace/projects/example-client-app/src/app/customapp/customapp.component.ts b/angular-workspace/projects/example-client-app/src/app/customapp/customapp.component.ts
index d39d1b18fc..aecf4ecb72 100644
--- a/angular-workspace/projects/example-client-app/src/app/customapp/customapp.component.ts
+++ b/angular-workspace/projects/example-client-app/src/app/customapp/customapp.component.ts
@@ -3,6 +3,7 @@ import { Component, Inject, ViewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { DrawerLocation, MenuItem, NimbleDialogDirective, NimbleDrawerDirective, OptionNotFound, OPTION_NOT_FOUND, UserDismissed } from '@ni/nimble-angular';
import type { TableRecord } from '@ni/nimble-angular/table';
+import { NimbleRichTextEditorDirective } from '@ni/nimble-angular/rich-text-editor';
import { BehaviorSubject, Observable } from 'rxjs';
interface ComboboxItem {
@@ -44,7 +45,7 @@ export class CustomAppComponent {
public selectedRadio = 'mango';
public activeTabId = 'tab-1';
public activeAnchorTabId = 'a-tab-2';
- public markdownString = `Supported rich text formatting options:
+ public viewerMarkdownString = `Supported rich text formatting options:
1. **Bold**
2. *Italics*
3. Numbered lists
@@ -56,11 +57,23 @@ export class CustomAppComponent {
5. Absolute link:
`;
+ public editorMarkdownString = `Supported rich text formatting options:
+1. **Bold**
+2. *Italics*
+3. Numbered lists
+ 1. Option 1
+ 2. Option 2
+4. Bulleted lists
+ * Option 1
+ * Option 2
+`;
+
public readonly tableData$: Observable;
private readonly tableDataSubject = new BehaviorSubject([]);
@ViewChild('dialog', { read: NimbleDialogDirective }) private readonly dialog: NimbleDialogDirective;
@ViewChild('drawer', { read: NimbleDrawerDirective }) private readonly drawer: NimbleDrawerDirective;
+ @ViewChild('editor', { read: NimbleRichTextEditorDirective }) private readonly editor: NimbleRichTextEditorDirective;
public constructor(@Inject(ActivatedRoute) public readonly route: ActivatedRoute) {
this.tableData$ = this.tableDataSubject.asObservable();
@@ -115,4 +128,8 @@ export class CustomAppComponent {
});
this.tableDataSubject.next(tableData);
}
+
+ public loadRichTextEditorContent(): void {
+ this.editor.setMarkdown(this.editorMarkdownString);
+ }
}
diff --git a/angular-workspace/projects/ni/nimble-angular/rich-text-editor/ng-package.json b/angular-workspace/projects/ni/nimble-angular/rich-text-editor/ng-package.json
new file mode 100644
index 0000000000..7945e60e70
--- /dev/null
+++ b/angular-workspace/projects/ni/nimble-angular/rich-text-editor/ng-package.json
@@ -0,0 +1,6 @@
+{
+ "$schema": "../../../../../node_modules/ng-packagr/ng-package.schema.json",
+ "lib": {
+ "entryFile": "public-api.ts"
+ }
+}
\ No newline at end of file
diff --git a/angular-workspace/projects/ni/nimble-angular/rich-text-editor/nimble-rich-text-editor.directive.ts b/angular-workspace/projects/ni/nimble-angular/rich-text-editor/nimble-rich-text-editor.directive.ts
new file mode 100644
index 0000000000..abd5a2a0a8
--- /dev/null
+++ b/angular-workspace/projects/ni/nimble-angular/rich-text-editor/nimble-rich-text-editor.directive.ts
@@ -0,0 +1,83 @@
+import { Directive, ElementRef, EventEmitter, HostListener, Input, Output, Renderer2 } from '@angular/core';
+import type { RichTextEditor } from '@ni/nimble-components/dist/esm/rich-text-editor';
+import { BooleanValueOrAttribute, toBooleanProperty } from '@ni/nimble-angular/internal-utilities';
+
+export type { RichTextEditor };
+
+/**
+ * Directive to provide Angular integration for the rich text editor element.
+ */
+@Directive({
+ selector: 'nimble-rich-text-editor'
+})
+
+export class NimbleRichTextEditorDirective {
+ @Output() public inputEvent = new EventEmitter();
+
+ public get disabled(): boolean {
+ return this.elementRef.nativeElement.disabled;
+ }
+
+ @Input() public set disabled(value: BooleanValueOrAttribute) {
+ this.renderer.setProperty(this.elementRef.nativeElement, 'disabled', toBooleanProperty(value));
+ }
+
+ public get footerHidden(): boolean {
+ return this.elementRef.nativeElement.footerHidden;
+ }
+
+ // Renaming because property should have camel casing, but attribute should not
+ // eslint-disable-next-line @angular-eslint/no-input-rename
+ @Input('footer-hidden') public set footerHidden(value: BooleanValueOrAttribute) {
+ this.renderer.setProperty(this.elementRef.nativeElement, 'footerHidden', toBooleanProperty(value));
+ }
+
+ public get errorVisible(): boolean {
+ return this.elementRef.nativeElement.errorVisible;
+ }
+
+ // Renaming because property should have camel casing, but attribute should not
+ // eslint-disable-next-line @angular-eslint/no-input-rename
+ @Input('error-visible') public set errorVisible(value: BooleanValueOrAttribute) {
+ this.renderer.setProperty(this.elementRef.nativeElement, 'errorVisible', toBooleanProperty(value));
+ }
+
+ public get errorText(): string | undefined {
+ return this.elementRef.nativeElement.errorText;
+ }
+
+ // Renaming because property should have camel casing, but attribute should not
+ // eslint-disable-next-line @angular-eslint/no-input-rename
+ @Input('error-text') public set errorText(value: string | undefined) {
+ this.renderer.setProperty(this.elementRef.nativeElement, 'errorText', value);
+ }
+
+ public get placeholder(): string | undefined {
+ return this.elementRef.nativeElement.placeholder;
+ }
+
+ @Input() public set placeholder(value: string | undefined) {
+ this.renderer.setProperty(this.elementRef.nativeElement, 'placeholder', value);
+ }
+
+ public constructor(private readonly renderer: Renderer2, private readonly elementRef: ElementRef) { }
+
+ public getMarkdown(): string {
+ return this.elementRef.nativeElement.getMarkdown();
+ }
+
+ public setMarkdown(value: string): void {
+ this.elementRef.nativeElement.setMarkdown(value);
+ }
+
+ public get empty(): boolean {
+ return this.elementRef.nativeElement.empty;
+ }
+
+ @HostListener('input', ['$event'])
+ public onInput($event: Event): void {
+ if ($event.target === this.elementRef.nativeElement) {
+ this.inputEvent.emit();
+ }
+ }
+}
diff --git a/angular-workspace/projects/ni/nimble-angular/rich-text-editor/nimble-rich-text-editor.module.ts b/angular-workspace/projects/ni/nimble-angular/rich-text-editor/nimble-rich-text-editor.module.ts
new file mode 100644
index 0000000000..0c6c57ce98
--- /dev/null
+++ b/angular-workspace/projects/ni/nimble-angular/rich-text-editor/nimble-rich-text-editor.module.ts
@@ -0,0 +1,12 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { NimbleRichTextEditorDirective } from './nimble-rich-text-editor.directive';
+
+import '@ni/nimble-components/dist/esm/rich-text-editor';
+
+@NgModule({
+ declarations: [NimbleRichTextEditorDirective],
+ imports: [CommonModule],
+ exports: [NimbleRichTextEditorDirective]
+})
+export class NimbleRichTextEditorModule { }
diff --git a/angular-workspace/projects/ni/nimble-angular/rich-text-editor/public-api.ts b/angular-workspace/projects/ni/nimble-angular/rich-text-editor/public-api.ts
new file mode 100644
index 0000000000..fe7585d3da
--- /dev/null
+++ b/angular-workspace/projects/ni/nimble-angular/rich-text-editor/public-api.ts
@@ -0,0 +1,2 @@
+export * from './nimble-rich-text-editor.directive';
+export * from './nimble-rich-text-editor.module';
\ No newline at end of file
diff --git a/angular-workspace/projects/ni/nimble-angular/rich-text-editor/testing/ng-package.json b/angular-workspace/projects/ni/nimble-angular/rich-text-editor/testing/ng-package.json
new file mode 100644
index 0000000000..e5440110fb
--- /dev/null
+++ b/angular-workspace/projects/ni/nimble-angular/rich-text-editor/testing/ng-package.json
@@ -0,0 +1,6 @@
+{
+ "$schema": "../../../../../../node_modules/ng-packagr/ng-package.schema.json",
+ "lib": {
+ "entryFile": "public-api.ts"
+ }
+}
\ No newline at end of file
diff --git a/angular-workspace/projects/ni/nimble-angular/rich-text-editor/testing/public-api.ts b/angular-workspace/projects/ni/nimble-angular/rich-text-editor/testing/public-api.ts
new file mode 100644
index 0000000000..82a3ed3c52
--- /dev/null
+++ b/angular-workspace/projects/ni/nimble-angular/rich-text-editor/testing/public-api.ts
@@ -0,0 +1 @@
+export * from './rich-text-editor.pageobject';
\ No newline at end of file
diff --git a/angular-workspace/projects/ni/nimble-angular/rich-text-editor/testing/rich-text-editor.pageobject.ts b/angular-workspace/projects/ni/nimble-angular/rich-text-editor/testing/rich-text-editor.pageobject.ts
new file mode 100644
index 0000000000..075882fc1b
--- /dev/null
+++ b/angular-workspace/projects/ni/nimble-angular/rich-text-editor/testing/rich-text-editor.pageobject.ts
@@ -0,0 +1,5 @@
+import { RichTextEditorPageObject } from '@ni/nimble-components/dist/esm/rich-text-editor/testing/rich-text-editor.pageobject';
+import type { ToolbarButton } from '@ni/nimble-components/dist/esm/rich-text-editor/testing/types';
+
+export { RichTextEditorPageObject };
+export type { ToolbarButton };
\ No newline at end of file
diff --git a/angular-workspace/projects/ni/nimble-angular/rich-text-editor/tests/nimble-rich-text-editor.directive.spec.ts b/angular-workspace/projects/ni/nimble-angular/rich-text-editor/tests/nimble-rich-text-editor.directive.spec.ts
new file mode 100644
index 0000000000..2646bb0448
--- /dev/null
+++ b/angular-workspace/projects/ni/nimble-angular/rich-text-editor/tests/nimble-rich-text-editor.directive.spec.ts
@@ -0,0 +1,349 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { Component, ElementRef, ViewChild } from '@angular/core';
+import type { BooleanValueOrAttribute } from '@ni/nimble-angular/internal-utilities';
+import { NimbleRichTextEditorModule } from '../nimble-rich-text-editor.module';
+import { NimbleRichTextEditorDirective, RichTextEditor } from '../nimble-rich-text-editor.directive';
+
+describe('Nimble Rich Text Editor', () => {
+ describe('module', () => {
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [NimbleRichTextEditorModule]
+ });
+ });
+
+ it('custom element is defined', () => {
+ expect(customElements.get('nimble-rich-text-editor')).not.toBeUndefined();
+ });
+ });
+
+ describe('with no values in template', () => {
+ @Component({
+ template: `
+
+ `
+ })
+ class TestHostComponent {
+ @ViewChild('editor', { read: NimbleRichTextEditorDirective }) public directive: NimbleRichTextEditorDirective;
+ @ViewChild('editor', { read: ElementRef }) public elementRef: ElementRef;
+ }
+
+ let fixture: ComponentFixture;
+ let directive: NimbleRichTextEditorDirective;
+ let nativeElement: RichTextEditor;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [TestHostComponent],
+ imports: [NimbleRichTextEditorModule]
+ });
+ fixture = TestBed.createComponent(TestHostComponent);
+ fixture.detectChanges();
+ directive = fixture.componentInstance.directive;
+ nativeElement = fixture.componentInstance.elementRef.nativeElement;
+ });
+
+ it('has expected defaults for markdown', () => {
+ expect(directive.getMarkdown()).toBe('');
+ expect(nativeElement.getMarkdown()).toBe('');
+ });
+
+ it('has expected defaults for disabled', () => {
+ expect(directive.disabled).toBeFalse();
+ expect(nativeElement.disabled).toBeFalse();
+ });
+
+ it('has expected defaults for footerHidden', () => {
+ expect(directive.footerHidden).toBeFalse();
+ expect(nativeElement.footerHidden).toBeFalse();
+ });
+
+ it('has expected defaults for errorVisible', () => {
+ expect(directive.errorVisible).toBeFalse();
+ expect(nativeElement.errorVisible).toBeFalse();
+ });
+
+ it('has expected defaults for errorText', () => {
+ expect(directive.errorText).toBeUndefined();
+ expect(nativeElement.errorText).toBeUndefined();
+ });
+
+ it('has expected defaults for placeholder', () => {
+ expect(directive.placeholder).toBeUndefined();
+ expect(nativeElement.placeholder).toBeUndefined();
+ });
+
+ it('has expected defaults for empty', () => {
+ expect(directive.empty).toBeTrue();
+ expect(nativeElement.empty).toBeTrue();
+ });
+ });
+
+ describe('with template string values', () => {
+ @Component({
+ template: `
+
+ `
+ })
+ class TestHostComponent {
+ @ViewChild('editor', { read: NimbleRichTextEditorDirective }) public directive: NimbleRichTextEditorDirective;
+ @ViewChild('editor', { read: ElementRef }) public elementRef: ElementRef;
+ }
+
+ let fixture: ComponentFixture;
+ let directive: NimbleRichTextEditorDirective;
+ let nativeElement: RichTextEditor;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [TestHostComponent],
+ imports: [NimbleRichTextEditorModule]
+ });
+ fixture = TestBed.createComponent(TestHostComponent);
+ fixture.detectChanges();
+ directive = fixture.componentInstance.directive;
+ nativeElement = fixture.componentInstance.elementRef.nativeElement;
+ });
+
+ it('will use template string values for disabled', () => {
+ expect(directive.disabled).toBeTrue();
+ expect(nativeElement.disabled).toBeTrue();
+ });
+
+ it('will use template string values for footerHidden', () => {
+ expect(directive.footerHidden).toBeTrue();
+ expect(nativeElement.footerHidden).toBeTrue();
+ });
+
+ it('will use template string values for errorVisible', () => {
+ expect(directive.errorVisible).toBeTrue();
+ expect(nativeElement.errorVisible).toBeTrue();
+ });
+
+ it('will use template string values for errorText', () => {
+ expect(directive.errorText).toBe('Error text');
+ expect(nativeElement.errorText).toBe('Error text');
+ });
+
+ it('will use template string values for placeholder', () => {
+ expect(directive.placeholder).toBe('Placeholder value');
+ expect(nativeElement.placeholder).toBe('Placeholder value');
+ });
+ });
+
+ describe('with property bound values', () => {
+ @Component({
+ template: `
+
+ `
+ })
+ class TestHostComponent {
+ @ViewChild('editor', { read: NimbleRichTextEditorDirective }) public directive: NimbleRichTextEditorDirective;
+ @ViewChild('editor', { read: ElementRef }) public elementRef: ElementRef;
+ public disabled = false;
+ public footerHidden = false;
+ public errorVisible = false;
+ public errorText = 'initial';
+ public placeholder = 'initial';
+ }
+
+ let fixture: ComponentFixture;
+ let directive: NimbleRichTextEditorDirective;
+ let nativeElement: RichTextEditor;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [TestHostComponent],
+ imports: [NimbleRichTextEditorModule]
+ });
+ fixture = TestBed.createComponent(TestHostComponent);
+ fixture.detectChanges();
+ directive = fixture.componentInstance.directive;
+ nativeElement = fixture.componentInstance.elementRef.nativeElement;
+ nativeElement.setMarkdown('1. ***Numbered list with bold and italics***\n\n* ___Bulleted list with bold and italics___');
+ fixture.detectChanges();
+ });
+
+ it('can be configured with method for markdown', () => {
+ expect(directive.getMarkdown()).toBe('1. ***Numbered list with bold and italics***\n\n* ***Bulleted list with bold and italics***');
+ expect(nativeElement.getMarkdown()).toBe('1. ***Numbered list with bold and italics***\n\n* ***Bulleted list with bold and italics***');
+
+ nativeElement.setMarkdown('**Updated value**');
+ fixture.detectChanges();
+
+ expect(directive.getMarkdown()).toBe('**Updated value**');
+ expect(nativeElement.getMarkdown()).toBe('**Updated value**');
+ });
+
+ it('can be configured with property binding for disabled', () => {
+ expect(directive.disabled).toBeFalse();
+ expect(nativeElement.disabled).toBeFalse();
+
+ fixture.componentInstance.disabled = true;
+ fixture.detectChanges();
+
+ expect(directive.disabled).toBeTrue();
+ expect(nativeElement.disabled).toBeTrue();
+ });
+
+ it('can be configured with property binding for footerHidden', () => {
+ expect(directive.footerHidden).toBeFalse();
+ expect(nativeElement.footerHidden).toBeFalse();
+
+ fixture.componentInstance.footerHidden = true;
+ fixture.detectChanges();
+
+ expect(directive.footerHidden).toBeTrue();
+ expect(nativeElement.footerHidden).toBeTrue();
+ });
+
+ it('can be configured with property binding for errorVisible', () => {
+ expect(directive.errorVisible).toBeFalse();
+ expect(nativeElement.errorVisible).toBeFalse();
+
+ fixture.componentInstance.errorVisible = true;
+ fixture.detectChanges();
+
+ expect(directive.errorVisible).toBeTrue();
+ expect(nativeElement.errorVisible).toBeTrue();
+ });
+
+ it('can be configured with property binding for errorText', () => {
+ expect(directive.errorText).toBe('initial');
+ expect(nativeElement.errorText).toBe('initial');
+
+ fixture.componentInstance.errorText = 'updated error text';
+ fixture.detectChanges();
+
+ expect(directive.errorText).toBe('updated error text');
+ expect(nativeElement.errorText).toBe('updated error text');
+ });
+
+ it('can be configured with property binding for placeholder', () => {
+ expect(directive.placeholder).toBe('initial');
+ expect(nativeElement.placeholder).toBe('initial');
+
+ fixture.componentInstance.placeholder = 'updated placeholder value';
+ fixture.detectChanges();
+
+ expect(directive.placeholder).toBe('updated placeholder value');
+ expect(nativeElement.placeholder).toBe('updated placeholder value');
+ });
+
+ it('updates empty when markdown is cleared', () => {
+ expect(directive.empty).toBeFalse();
+ expect(nativeElement.empty).toBeFalse();
+
+ nativeElement.setMarkdown('');
+ fixture.detectChanges();
+
+ expect(directive.empty).toBeTrue();
+ expect(nativeElement.empty).toBeTrue();
+ });
+ });
+
+ describe('with attribute bound values', () => {
+ @Component({
+ template: `
+
+ `
+ })
+ class TestHostComponent {
+ @ViewChild('editor', { read: NimbleRichTextEditorDirective }) public directive: NimbleRichTextEditorDirective;
+ @ViewChild('editor', { read: ElementRef }) public elementRef: ElementRef;
+ public disabled: BooleanValueOrAttribute = null;
+ public footerHidden: BooleanValueOrAttribute = null;
+ public errorVisible: BooleanValueOrAttribute = null;
+ public errorText = 'initial';
+ public placeholder = 'initial';
+ }
+
+ let fixture: ComponentFixture;
+ let directive: NimbleRichTextEditorDirective;
+ let nativeElement: RichTextEditor;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [TestHostComponent],
+ imports: [NimbleRichTextEditorModule]
+ });
+ fixture = TestBed.createComponent(TestHostComponent);
+ fixture.detectChanges();
+ directive = fixture.componentInstance.directive;
+ nativeElement = fixture.componentInstance.elementRef.nativeElement;
+ });
+
+ it('can be configured with attribute binding for disabled', () => {
+ expect(directive.disabled).toBeFalse();
+ expect(nativeElement.disabled).toBeFalse();
+
+ fixture.componentInstance.disabled = '';
+ fixture.detectChanges();
+
+ expect(directive.disabled).toBeTrue();
+ expect(nativeElement.disabled).toBeTrue();
+ });
+
+ it('can be configured with attribute binding for footerHidden', () => {
+ expect(directive.footerHidden).toBeFalse();
+ expect(nativeElement.footerHidden).toBeFalse();
+
+ fixture.componentInstance.footerHidden = '';
+ fixture.detectChanges();
+
+ expect(directive.footerHidden).toBeTrue();
+ expect(nativeElement.footerHidden).toBeTrue();
+ });
+
+ it('can be configured with attribute binding for errorVisible', () => {
+ expect(directive.errorVisible).toBeFalse();
+ expect(nativeElement.errorVisible).toBeFalse();
+
+ fixture.componentInstance.errorVisible = '';
+ fixture.detectChanges();
+
+ expect(directive.errorVisible).toBeTrue();
+ expect(nativeElement.errorVisible).toBeTrue();
+ });
+
+ it('can be configured with attribute binding for errorText', () => {
+ expect(directive.errorText).toBe('initial');
+ expect(nativeElement.errorText).toBe('initial');
+
+ fixture.componentInstance.errorText = 'updated error text';
+ fixture.detectChanges();
+
+ expect(directive.errorText).toBe('updated error text');
+ expect(nativeElement.errorText).toBe('updated error text');
+ });
+
+ it('can be configured with attribute binding for placeholder', () => {
+ expect(directive.placeholder).toBe('initial');
+ expect(nativeElement.placeholder).toBe('initial');
+
+ fixture.componentInstance.placeholder = 'updated placeholder value';
+ fixture.detectChanges();
+
+ expect(directive.placeholder).toBe('updated placeholder value');
+ expect(nativeElement.placeholder).toBe('updated placeholder value');
+ });
+ });
+});
\ No newline at end of file
diff --git a/angular-workspace/projects/ni/nimble-angular/rich-text-viewer/testing/ng-package.json b/angular-workspace/projects/ni/nimble-angular/rich-text-viewer/testing/ng-package.json
new file mode 100644
index 0000000000..e5440110fb
--- /dev/null
+++ b/angular-workspace/projects/ni/nimble-angular/rich-text-viewer/testing/ng-package.json
@@ -0,0 +1,6 @@
+{
+ "$schema": "../../../../../../node_modules/ng-packagr/ng-package.schema.json",
+ "lib": {
+ "entryFile": "public-api.ts"
+ }
+}
\ No newline at end of file
diff --git a/angular-workspace/projects/ni/nimble-angular/rich-text-viewer/testing/public-api.ts b/angular-workspace/projects/ni/nimble-angular/rich-text-viewer/testing/public-api.ts
new file mode 100644
index 0000000000..88af2dc5ae
--- /dev/null
+++ b/angular-workspace/projects/ni/nimble-angular/rich-text-viewer/testing/public-api.ts
@@ -0,0 +1 @@
+export * from './rich-text-viewer.pageobject';
\ No newline at end of file
diff --git a/angular-workspace/projects/ni/nimble-angular/rich-text-viewer/testing/rich-text-viewer.pageobject.ts b/angular-workspace/projects/ni/nimble-angular/rich-text-viewer/testing/rich-text-viewer.pageobject.ts
new file mode 100644
index 0000000000..0deb789be0
--- /dev/null
+++ b/angular-workspace/projects/ni/nimble-angular/rich-text-viewer/testing/rich-text-viewer.pageobject.ts
@@ -0,0 +1,3 @@
+import { RichTextViewerPageObject } from '@ni/nimble-components/dist/esm/rich-text-viewer/testing/rich-text-viewer.pageobject';
+
+export { RichTextViewerPageObject };
\ No newline at end of file
diff --git a/change/@ni-nimble-angular-71e4b21c-6096-4e8c-85d5-d54801e0e17b.json b/change/@ni-nimble-angular-71e4b21c-6096-4e8c-85d5-d54801e0e17b.json
new file mode 100644
index 0000000000..9a49834bae
--- /dev/null
+++ b/change/@ni-nimble-angular-71e4b21c-6096-4e8c-85d5-d54801e0e17b.json
@@ -0,0 +1,7 @@
+{
+ "type": "patch",
+ "comment": "Angular integration for rich text editor component",
+ "packageName": "@ni/nimble-angular",
+ "email": "123377523+vivinkrishna-ni@users.noreply.github.com",
+ "dependentChangeType": "patch"
+}