Angular
In an Angular application, the Document Editor and the Document Viewer behave like other components: they sit in templates, appear on routes and exchange documents through services. What sets them apart is the ASP.NET Core backend they need and the global JavaScript objects that their API is called through. Developers who add both components to an existing Angular application find the pieces here: backend addresses per environment, a development proxy, a document service, calls from TypeScript, routes and hosting.
A first editor or viewer in a new Angular project is built in Document Editor: Angular and Document Viewer: Angular. The supported Angular versions and the setup of standalone applications are described in npm Packages: Angular. The code below uses an Angular 19 application with a root module, as the tutorials do.
How the Application Connects to the Backend
Angular application (ng serve, http://localhost:4200)
|
|-- tx-document-editor ---- WebSocket ----> /api/TXWebSocket
|-- tx-document-viewer ---- HTTP ---------> /TextControl (controller of the viewer)
|-- DocumentService ------- HTTP ---------> /Document (your controller)
v
ASP.NET Core backend with TX Text Control .NET Server, for example https://localhost:7118
One ASP.NET Core backend serves all three connections. Set it up as described in Configure Program.cs for Separate Frontends and add the controller of Add a Document Controller. Place a template.docx in the Documents folder of the backend.
During development, the Angular development server can forward all three connections to the backend, so the application calls its own origin and the backend needs no CORS policy for it. In production, the application uses the public address of the backend.
Add Both Components to the Application
Install both packages as npm Packages: Angular describes; an application with both needs an Angular version from 18.2.14 to 21. In an application with a root module, src/app/app.module.ts imports both modules, declares the components of your application that show them and provides HttpClient for the document service:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { provideHttpClient } from '@angular/common/http';
import { DocumentEditorModule } from '@txtextcontrol/tx-ng-document-editor';
import { DocumentViewerModule } from '@txtextcontrol/tx-ng-document-viewer';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { EditorComponent } from './editor/editor.component';
import { ViewerComponent } from './viewer/viewer.component';
({
declarations: [
AppComponent,
EditorComponent,
ViewerComponent
],
imports: [
BrowserModule,
AppRoutingModule,
DocumentEditorModule,
DocumentViewerModule
],
providers: [provideHttpClient()],
bootstrap: [AppComponent]
})
export class AppModule { }
DocumentEditorModule provides the tx-document-editor component and DocumentViewerModule the tx-document-viewer component. EditorComponent and ViewerComponent are the components of your application that show them; their code follows below. The routes in src/app/app-routing.module.ts map them, for example to editor and viewer.
Set the Backend Address per Environment
The editor, the viewer and the document service need the address of the backend, and the address differs between development and production. Angular environment files keep it in one place per build configuration. Create them with the Angular CLI:
ng generate environments
The command creates src/environments/environment.ts and src/environments/environment.development.ts and configures angular.json to replace the first file with the second in the development configuration, which ng serve uses. ng build uses the production configuration and environment.ts.
Set the production addresses in src/environments/environment.ts:
export const environment = {
backendUrl: 'https://backend.example.com',
webSocketUrl: 'wss://backend.example.com/api/TXWebSocket'
};
During development, the application uses the address of the development server, which forwards the requests to the backend. Set them in src/environments/environment.development.ts:
export const environment = {
backendUrl: 'http://localhost:4200',
webSocketUrl: 'ws://localhost:4200/api/TXWebSocket'
};
The files are part of the application that the browser loads, so they must not contain secrets. The editor connects with wss, or ws when the address uses HTTP. The components read their inputs once, when they're initialized; changing a bound value such as webSocketURL later has no effect on a component that already runs.
Proxy the Backend During Development
Create src/proxy.conf.json, which forwards the paths of the editor, the viewer and the document controller to the backend:
{
"/api/TXWebSocket": {
"target": "https://localhost:7118",
"secure": false,
"ws": true
},
"/TextControl": {
"target": "https://localhost:7118",
"secure": false
},
"/Document": {
"target": "https://localhost:7118",
"secure": false
}
}
Then add the file to the options of the serve target in angular.json:
"serve": {
"options": {
"proxyConfig": "src/proxy.conf.json"
}
}
The code works as follows:
/api/TXWebSocketcovers the script of the editor and its WebSocket connection."ws": trueforwards the WebSocket connection as well./TextControlcovers the requests of the viewer, and/Documentthe document controller."secure": falseaccepts the development certificate of the backend. Replace the port with the HTTPS port of your backend.
Restart ng serve after you change the proxy configuration. If the proxy reports ECONNREFUSED for a localhost target, use 127.0.0.1 in the target instead. Without the proxy, set the backend address in environment.development.ts directly and allow the origin http://localhost:4200 in the CORS policy of the backend.
Load and Save Documents Through a Service
A service encapsulates the calls to the document controller of the backend. Create src/app/document.service.ts:
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from '../environments/environment';
export interface DocumentData {
name: string;
data: string;
}
({ providedIn: 'root' })
export class DocumentService {
constructor(private http: HttpClient) {}
load(name: string) {
return this.http.get<DocumentData>(environment.backendUrl + '/Document/Load', { params: { name: name } });
}
save(document: DocumentData) {
return this.http.post<boolean>(environment.backendUrl + '/Document/Save', document);
}
}
DocumentData matches the class of the controller: the name of the document and its base64-encoded content, the form in which both components save and load binary formats.
Program the Editor From TypeScript
The editor's API is the global TXTextControl object, which the component loads with its script. When the object is available, the component fires the txDocumentEditorLoaded event on document; the editor is ready for API calls after its own textControlLoaded event. src/app/editor/editor.component.ts loads the template when the editor is ready, saves it back on request and removes the editor when the route changes:
import { Component, HostListener, OnDestroy } from '@angular/core';
import { DocumentService } from '../document.service';
import { environment } from '../../environments/environment';
declare const TXTextControl: any;
({
selector: 'app-editor',
templateUrl: './editor.component.html',
standalone: false
})
export class EditorComponent implements OnDestroy {
webSocketUrl = environment.webSocketUrl;
constructor(private documents: DocumentService) {}
('document:txDocumentEditorLoaded')
onTxDocumentEditorLoaded() {
TXTextControl.addEventListener('textControlLoaded', () => {
this.documents.load('template.docx').subscribe(result => {
TXTextControl.loadDocument(TXTextControl.StreamType.WordprocessingML, result.data);
});
});
}
save() {
TXTextControl.saveDocument(TXTextControl.StreamType.WordprocessingML, (result: any) => {
this.documents.save({ name: 'template.docx', data: result.data }).subscribe();
});
}
ngOnDestroy() {
// Close the connection and remove the editor before Angular destroys the component
if (typeof TXTextControl !== 'undefined') {
TXTextControl.removeFromDom();
}
}
}
The template src/app/editor/editor.component.html binds the address and adds a button:
<button (click)="save()">Save</button>
<tx-document-editor
width="1000px"
height="600px"
[webSocketURL]="webSocketUrl">
</tx-document-editor>
The code works as follows:
declare const TXTextControl: any;tells TypeScript about the global TXTextControl object. The packages include no type definitions for it.@HostListener('document:txDocumentEditorLoaded')runs when the script is loaded. addEventListener then waits fortextControlLoadedbefore the document is requested.- loadDocument replaces the content of the editor with the base64-encoded DOCX from the service.
- saveDocument passes a SaveDocumentResult to its callback, whose data the service posts to the backend.
ngOnDestroyruns when Angular destroys the component, for example when the user navigates to another route. TXTextControl.removeFromDom closes the WebSocket connection gracefully and removes the editor, its event handlers and its CSS references.
To change settings such as the edit mode while the editor runs, call the API instead of changing the bound input, for example TXTextControl.setEditMode. Loading and Saving Documents and JavaScript API describe the API in detail.
Program the Viewer From TypeScript
The viewer's API is the global TXDocumentViewer object, and the viewer fires documentViewerLoaded on window when it's ready. Request the document inside the handler, not earlier, so that it never arrives before the viewer can take it. src/app/viewer/viewer.component.ts:
import { Component, HostListener } from '@angular/core';
import { DocumentService } from '../document.service';
import { environment } from '../../environments/environment';
declare const TXDocumentViewer: any;
({
selector: 'app-viewer',
templateUrl: './viewer.component.html',
standalone: false
})
export class ViewerComponent {
backendUrl = environment.backendUrl;
constructor(private documents: DocumentService) {}
('window:documentViewerLoaded')
onDocumentViewerLoaded() {
this.documents.load('template.docx').subscribe(result => {
TXDocumentViewer.loadDocument(result.data, result.name);
});
}
async saveAsPdf() {
const result = await TXDocumentViewer.saveDocument(TXDocumentViewer.StreamType.AdobePDF,
{ mergeFormFields: true, embedAnnotations: true });
const data = await result.base64();
this.documents.save({ name: 'template.pdf', data: data }).subscribe();
}
}
The template src/app/viewer/viewer.component.html:
<button (click)="saveAsPdf()">Save as PDF</button>
<tx-document-viewer
width="1000px"
height="600px"
[basePath]="backendUrl">
</tx-document-viewer>
The code works as follows:
- TXDocumentViewer.loadDocument takes the base64-encoded document and its name.
- TXDocumentViewer.saveDocument returns a promise. The DocumentSaveSettings merge the form field values and embed the annotations into the PDF, and base64 returns the saved document as base64.
basePathis the address of the backend, during development the address of the development server. When the application calls the backend on another origin, the backend's CORS policy must allow it.
Saving Documents in the Document Viewer section describes the formats and settings.
Use the Editor on Routes
Changing the route doesn't reload an Angular application. When a route shows the editor a second time without removing the first instance, the new component fails with the error "The editor is already initialized." Choose one of two approaches:
- Remove the editor with the component. Call
TXTextControl.removeFromDominngOnDestroy, as theEditorComponentabove does. Each visit of the route then creates a new editor with a new connection and loads the document again. Save the document before the user leaves the route; unsaved content is lost. - Keep one editor alive. Place the editor in a component that is always rendered, and move its element into a placeholder of each routed component that shows it. A
CanDeactivateguard moves the editor back and resets its content before the route changes. Moving the editor avoids a new initialization on the server and in the browser. The blog article Reuse Persistent Document Editor Components in Angular SPA Applications and the repository TXTextControl.Angular.SPA contain the complete implementation.
Pass Access Tokens
When the backend protects the editor and the viewer with access tokens, the Angular application requests a token from its backend first. The Angular component of the editor has no setting for additional WebSocket sub-protocols, so the Text Control Angular security sample adds the token to the query string of webSocketURL and creates the component only after the token is available:
<div *ngIf="accessToken">
<tx-document-editor
width="1000px"
height="600px"
webSocketURL="wss://backend.example.com/api/TXWebSocket?access_token={{ accessToken }}">
</tx-document-editor>
</div>
The viewer component has no setting for a token: it splits a query string off basePath and sends it only with the request for the viewer script, so the other requests of the viewer carry no token. Serve the application from the origin of the backend and protect the viewer with the sign-in cookie of the user, as Frontends on Another Origin describes.
Tokens in URLs can appear in the logs of web servers and proxies. Access Tokens and WebSocket Security describe the server side and the alternatives. When users sign in to the backend with a cookie instead, Require Signed-In Users protects the endpoints without tokens in the URL. The blog article Implementing a Security Middleware for Angular Document Editor Applications in C# shows the complete token sample.
Use a Node.js WebSocket Server
The editor component doesn't depend on ASP.NET Core: any server that provides the TX Text Control WebSocket endpoint works. A Node.js application with the WebSocketServer class of the @txtextcontrol/tx-websocket-server package can take the place of the middleware and forward the connection to the Document Editor backend. Document Editor: Node.js sets up the backend and the WebSocketServer, and Connect to a Backend on Another Machine lists its settings. Point the component to the endpoint of the Node.js application:
<tx-document-editor
width="1000px"
height="500px"
webSocketURL="ws://localhost:8080/TXWebSocket">
</tx-document-editor>
WebSocketServer also serves the editor script that the component requests from /TXWebSocket/GetResource. The Node.js application covers only the editor: the Document Viewer and your own document endpoints, such as the DocumentService above, still need an ASP.NET Core backend. The blog article Creating an Angular Document Editor Application with a Node.js WebSocket Server builds the same setup. It dates from before version 33.0, when the backend was a Windows service, so start the backend as the Getting Started tutorial describes.
Build and Host the Application
ng build compiles the application with the production configuration and environment.ts. With the application builder of current Angular versions, the files to deploy are in dist/project-name/browser, where project-name is the name of your project. If the application doesn't run at the root of its server, set its base URL with ng build --base-href. Two setups are common:
- Separate hosting. The Angular application and the backend run on different servers or origins. environment.ts contains the public address of the backend, and the CORS policy of the backend allows the origin of the application.
- One ASP.NET Core project serves both. Visual Studio's project templates for Angular with ASP.NET Core create a client project and a server project. In development, the server starts the Angular development server; in production, it serves the built application with
app.UseDefaultFiles(),app.UseStaticFiles()andapp.MapFallbackToFile("/index.html"). The application and the backend then share one origin, and environment.ts contains that origin.
Behind a load balancer, keep every WebSocket connection of an editor on the same server, as Scaling and Load Balancing explains. Host the Connection describes proxies and hosting platforms.
Next Steps
- WebSocket Architecture: the connection between the editor and the backend in detail.
- ASP.NET Core: the backend for the Angular application.
- JavaScript API: program the editor from TypeScript.
- REST Services: call document services from Angular.