Seamless Real-Time Word Document Collaboration in Angular Using Redis

syncfusionsyncfusion
19 min read

TL;DR: Learn to build a seamless real-time Word document collaboration system using Angular, ASP.NET Core, Syncfusion’s Document Editor, Redis, SignalR, and Azure Blob Storage. This guide shows you how to implement fast caching with Redis, enable real-time updates with SignalR, and ensure secure file storage, creating an efficient multi-user editing experience.

In today’s fast-paced digital workspace, enabling teams to collaborate on documents in real time is no longer a luxury; it’s necessary. Whether multiple users are editing a project proposal or co-authoring a report, real-time document collaboration brings agility and efficiency to the workflow. However, implementing this feature at scale involves more than just syncing keystrokes; it demands a reliable architecture that ensures accuracy, consistency, and performance.

With Syncfusion® Angular Document Editor, paired with Redis for real-time synchronization and Azure Blob Storage for secure file handling, developers can implement a fully scalable and reliable co-authoring experience

In this blog, you’ll learn how to:

  • Configure Redis for caching and instant data synchronization using ASP.NET Core Web API.

  • Store and access documents securely via Azure Blob Storage.

  • Use the Syncfusion® File Manager to browse and manage files.

  • Build a real-time editing UI with Syncfusion® Angular Document Editor, enhanced with SignalR.

Let’s begin by setting up Redis as the caching mechanism to coordinate real-time user’s changes.

Setting up the Redis cache

To enable real-time collaborative editing, a fast and reliable caching system is essential. Redis is a high-performance in-memory store that temporarily queues user edits, handles conflict resolution using the Operational Transformation algorithm, and keeps all users in sync.

In this section, we’ll walk you through setting up Azure Cache for Redis, retrieving the necessary connection details, and integrating it with your backend to power seamless document collaboration.

Step 1: Create a Redis cache

Sign in to the Azure portal, click the Create button, select Azure Cache for Redis from the list of services, and fill in the required details.

<alt-text>

Creating a Redis cache

Step 2: Retrieve connection details

To connect your backend to the Redis cache, you’ll need the connection string, which can be found under the Access Keys section of your Redis instance in the Azure portal.

<alt-text>

Retrieving connection details

Setting up ASP.NET Core Web API backend

To handle real-time collaboration, file storage, and synchronization services, set up the ASP.NET Core backend.

Step 1: Create a new ASP.NET Core Web API project

Set up a new ASP.NET Core Web API project in Visual Studio. Install the required NuGet packages to support collaborative editing features, including Redis, Azure Blob Storage, SignalR, and Syncfusion’s Document Editor.

Required NuGet packages

  • StackExchange.Redis Enables real-time data storage and retrieval, ensuring smooth synchronization of document changes across users.

  • Microsoft.Azure.SignalR Facilitates real-time communication between clients and the server using Azure’s managed SignalR service, enhancing scalability and reducing server load.

  • Microsoft.AspNetCore.SignalR.StackExchangeRedis Integrates SignalR with Redis to broadcast real-time document updates across multiple server instances.

  • Azure.Storage.Blobs This package provides secure cloud storage for accessing and retrieving Word documents from Azure Blob Storage.

  • Syncfusion.EJ2.WordEditor.AspNet.Core Powers backend processing for the Syncfusion Document Editor, enabling real-time collaborative editing capabilities.

Step 2: Configure Redis and Azure Blob storage

Add the Redis connection string and Azure Storage account details to the appsettings.json file. This configuration enables seamless integration with Redis for real-time synchronization and Azure Blob Storage for secure document access.

appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "RedisConnectionString": "<YOUR REDIS CONNECTION STRING>"
  },
  "accountName": "<YOUR STORAGE ACCOUNT NAME>",
  "accountKey": "<YOUR ACCOUNT KEY>",
  "connectionString": "<YOUR CONNECTION STRING>",
  "containerName": "<YOUR CONATINER NAME>"
}

Step 3: Configure SignalR and Redis for real-time communication

To enable real-time collaborative editing, configure SignalR with Redis in your Program.cs file. This setup ensures document updates are instantly synchronized across connected clients.

Program.cs

using Microsoft.Azure.SignalR;
using StackExchange.Redis;
var config = builder.Configuration;
var redisConfig = config.GetSection("ConnectionStrings");
var connectionString = redisConfig["RedisConnectionString"];

// Configure SignalR with Redis
builder.Services.AddSignalR().AddStackExchangeRedis(connectionString, options =>
{
    options.Configuration.ChannelPrefix = "docedit";
});

// Configure Redis caching
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = connectionString;
});

// Register Redis connection multiplexer as a singleton
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
{
    var configuration = ConfigurationOptions.Parse(connectionString, true);
    return ConnectionMultiplexer.Connect(configuration);
});
………………..
// Map the SignalR hub
app.MapHub<DocumentEditorHub>("/documenteditorhub");

Step 4: Configure the SignalR hub for collaborative editing

Create a class named DocumentEditorHub.cs to manage collaborative editing sessions using SignalR and Redis. This hub facilitates real-time user connections , group management , and background task processing to ensure smooth synchronization of document updates.

Key methods in DocumentEditorHub.cs:

  • OnConnectedAsync: Assigns a unique connection ID to the client upon connection.

  • JoinGroup: This method uses a unique document-specific group (room) using a unique document ID. It checks if the room exists in the Redis cache, retrieves active users, and synchronizes them with the newly joined user.

  • OnDisconnectedAsync: Removes the user from Redis, updates the active users, and ensures that pending document edits are saved. If no users remain in the room, all unsaved changes are automatically saved to the source document.

Note: The DocumentEditorHub.cs class also includes additional methods for managing the collaborative editing sessions. You can explore these in the example project on the GitHub demo.

DocumentEditorHub.cs

// Called when a new connection is established
public override Task OnConnectedAsync()
{
    // Send session ID to client
    Clients.Caller.SendAsync("dataReceived", "connectionId", Context.ConnectionId);
    return base.OnConnectedAsync();
}

// Handles a user joining a group (room) for document editing
public async Task JoinGroup(ActionInfo info)
{
    // Set the connection ID to info
    info.ConnectionId = Context.ConnectionId;

    // Add the connection ID to the group
    await Groups.AddToGroupAsync(Context.ConnectionId, info.RoomName);

    // To ensure whether the room exists in the Redis cache
    bool roomExists = await _db.KeyExistsAsync(info.RoomName + CollaborativeEditingHelper.UserInfoSuffix);
    if (roomExists)
    {
        // Fetch all connected users from Redis
        var allUsers = await _db.HashGetAllAsync(info.RoomName + CollaborativeEditingHelper.UserInfoSuffix);
        var userList = allUsers.Select(u => JsonConvert.DeserializeObject<ActionInfo>(u.Value)).ToList();

        // Send the existing user details to the newly joined user
        await Clients.Caller.SendAsync("dataReceived", "addUser", userList);
    }

    // Add user to Redis
    await _db.HashSetAsync(info.RoomName + CollaborativeEditingHelper.UserInfoSuffix, Context.ConnectionId, JsonConvert.SerializeObject(info));

    // Store the room name with the connection ID
    await _db.HashSetAsync(CollaborativeEditingHelper.ConnectionIdRoomMappingKey, Context.ConnectionId, info.RoomName);

    // Notify all the existing users in the group about the new user
    await Clients.GroupExcept(info.RoomName, Context.ConnectionId).SendAsync("dataReceived", "addUser", info);
}

// Called when a user disconnects from the hub
public override async Task OnDisconnectedAsync(Exception? e)
{
    // Get the room name associated with the connection ID
    string roomName = await _db.HashGetAsync(CollaborativeEditingHelper.ConnectionIdRoomMappingKey, Context.ConnectionId);

    // Remove user from Redis
    await _db.HashDeleteAsync(roomName + CollaborativeEditingHelper.UserInfoSuffix, Context.ConnectionId);

    // Fetch all connected users from Redis
    var allUsers = await _db.HashGetAllAsync(roomName + CollaborativeEditingHelper.UserInfoSuffix);
    var userList = allUsers.Select(u => JsonConvert.DeserializeObject<ActionInfo>(u.Value)).ToList();

    // Remove connection to room name mapping
    await _db.HashDeleteAsync(CollaborativeEditingHelper.ConnectionIdRoomMappingKey, Context.ConnectionId);

    if (userList.Count == 0)
    {
        // Auto save the pending operations to the source document
        RedisValue[] pendingOps = await _db.ListRangeAsync(roomName, 0, -1);
        if (pendingOps.Length > 0)
        {
            List<ActionInfo> actions = new List<ActionInfo>();
            // Prepare the message to add it to the background service queue.
            foreach (var element in pendingOps)
            {
                actions.Add(JsonConvert.DeserializeObject<ActionInfo>(element.ToString()));
            }
            var message = new SaveInfo
            {
                Action = actions,
                PartialSave = false,
                RoomName = roomName,
            };
            // Queue the message for background processing and save the operations to the source document in the background task
            _ = saveTaskQueue.QueueBackgroundWorkItemAsync(message);
        }
    }
    else
    {
        // Notify remaining clients about the user disconnection
        await Clients.Group(roomName).SendAsync("dataReceived", "removeUser", Context.ConnectionId);
    }

    await base.OnDisconnectedAsync(e);
}

Step 5: Implement the Azure storage service

Create a service class named AzureDocumentStorageService.cs to handle file operations. You can refer to the implementation in the linked GitHub demo. This service manages interactions with Azure Blob Storage, such as retrieving files from Azure and displaying them using the Syncfusion® File Manager component.

To learn more about integrating Azure Blob Storage with Syncfusion® File Manager, refer to the blog post.

Step 6: Configure Web API Actions for collaborative editing

Create an API controller (Controllers /CollaborativeEditingController.cs ) to manage real-time document collaboration. This controller ensures smooth synchronization and efficient processing of user edits through the following key methods:

  • ImportFile: This method retrieves the document from Azure Blob Storage, applies any pending edits stored in Redis, and returns the updated content in SFDT format.

  • GetSourceDocumentFromAzureAsync: Loads a document from Azure Blob Storage and makes it available for editing.

  • GetPendingOperations: Retrieves pending edit actions from Redis.

  • UpdateAction: Saves user edits in Redis and broadcasts them via SignalR to keep all collaborators in sync.

Note: The CollaborativeEditingController**.cs** also includes additional methods to handle real-time document collaboration. You can explore these in the example project on GitHub.

CollaborativeEditingController.cs

/// <summary>
/// Imports a document from Azure Blob storage and applies any pending actions
/// from Redis to bring it up to date. Returns the final document in SFDT format.
/// </summary>
/// <param name="param">File info, including fileName and documentOwner (room ID).</param>
/// <returns>JSON-serialized DocumentContent with version and SFDT data.</returns>
[HttpPost]
[Route("ImportFile")]
[EnableCors("AllowAllOrigins")]
public async Task<string> ImportFile([FromBody] CollaborativeEditingServerSide.Model.FileInfo param)
{
    try
    {
        // Prepare a container for returning document data
        DocumentContent content = new DocumentContent();

        // Retrieve the source document from Azure
        Syncfusion.EJ2.DocumentEditor.WordDocument document = await GetSourceDocumentFromAzureAsync(param.fileName);

        // Get any pending actions for this document/room
        List<ActionInfo> actions = await GetPendingOperations(param.documentOwner, 0, -1);

        if (actions != null && actions.Count > 0)
        {
            // If pending actions exist, apply them to the document
            document.UpdateActions(actions);
        }

        // Serialize the updated document to SFDT format
        string sfdt = Newtonsoft.Json.JsonConvert.SerializeObject(document);
        content.version = 0;
        content.sfdt = sfdt;

        // Dispose of the document to free resources
        document.Dispose();

        // Return the serialized content as a JSON string
        return Newtonsoft.Json.JsonConvert.SerializeObject(content);
    }
    catch
    {
        // Return null on failure
        return null;
    }
}

internal static async Task<Syncfusion.EJ2.DocumentEditor.WordDocument> GetSourceDocumentFromAzureAsync(string documentName)
{
    // Build the blob path for the document.
    var blobPath = GenerateDocumentBlobPath(documentName);

    // Get a reference to the blob client for the specified document.
    var blobClient = CreateBlobClient(blobPath);

    // Download the blob content into a memory stream.
    MemoryStream stream = new MemoryStream();
    await blobClient.DownloadToAsync(stream);

    // Reset the stream's position to the beginning.
    stream.Position = 0;

    // Determine the file format from the extension.
    int index = documentName.LastIndexOf('.');
    string type = index > -1 && index < documentName.Length - 1 ? documentName.Substring(index) : ".docx";

    // Load the document using the Syncfusion DocumentEditor API.
    Syncfusion.EJ2.DocumentEditor.WordDocument document =
        Syncfusion.EJ2.DocumentEditor.WordDocument.Load(stream, GetFormatType(type));

    stream.Dispose();
    return document;
}

// Method to retrieve pending operations from a Redis list between specified indexes
public async Task<List<ActionInfo>> GetPendingOperations(string listKey, long startIndex, long endIndex)
{
    // Get the database connection from the Redis connection multiplexer
    var db = _redisConnection.GetDatabase();

    var result = (RedisResult[])await db.ScriptEvaluateAsync(
        CollaborativeEditingHelper.PendingOperations,
        new RedisKey[] { listKey, listKey + CollaborativeEditingHelper.ActionsToRemoveSuffix },
        new RedisValue[] { startIndex, endIndex });

    var processingValues = (RedisResult[])result[0];
    var listValues = (RedisResult[])result[1];

    // Initialize the list to hold ActionInfo objects
    var actionInfoList = new List<ActionInfo>();

    // Deserialize the operations from JSON to ActionInfo objects and store them in the list
    actionInfoList.AddRange(processingValues.Select(value =>
        Newtonsoft.Json.JsonConvert.DeserializeObject<ActionInfo>((string)value)));

    // Deserialize the operations from JSON to ActionInfo objects and return as a list
    actionInfoList.AddRange(listValues.Select(value =>
        Newtonsoft.Json.JsonConvert.DeserializeObject<ActionInfo>((string)value)));

    return actionInfoList;
}

// Updates the action, modifies it if necessary, and notifies clients in the same room about the change
[HttpPost]
[Route("UpdateAction")]
[EnableCors("AllowAllOrigins")]
public async Task<ActionInfo> UpdateAction(ActionInfo param)
{
    // Adds the action to the cache
    ActionInfo modifiedAction = await AddOperationsToCache(param);

    // Sends the updated action to all clients in the room to notify them of the change
    await _hubContext.Clients.Group(param.RoomName).SendAsync("dataReceived", "action", modifiedAction);

    return modifiedAction;
}

Setting up the Angular frontend (client-side)

To support real-time collaborative editing, configure the Angular frontend. This enables users to browse documents from Azure Blob Storage, open them in an editor, and collaborate seamlessly within a responsive and user-friendly interface.

User story: Performing collaborative editing in the UI

  • Launch the Angular application.

  • Use the File Manager to browse and view documents stored in Azure Blob Storage.

  • Select a document to edit.

  • A dialog appears prompting you to enter your username.

  • After entering the username, the document opens in the Word Document Editor.

  • Click the “Share” button to generate the collaborative editing URL.

  • Share this URL with other users.

  • Each user who opens the URL is prompted to enter their username.

  • Once joined, all users can edit the document simultaneously in real time.

The following sections explain how these steps are implemented in the Angular application, from setting up components to configuring backend services for real-time collaboration.

Step 1: Create an Angular application and add dependencies

Start creating a new Angular application and adding Syncfusion® Document Editor and File Manager to enable real-time document collaboration with Azure Blob Storage.

Here’s how each component contributes to the solution:

  • File Manager: Provides an intuitive interface to browse and select documents stored in Azure Blob Storage for editing.

  • Word Document Editor: Supports real-time editing, allowing multiple users to collaborate on Word documents simultaneously.

Step 2: Manage and open files for collaborative editing

To enable users to browse and open documents for collaborative editing, the File Manager component is integrated with Azure Blob Storage. This process has two key parts:

a. Setting Up the File Manager UI

The File Manager component is added to files.component.html to provide an interactive interface for browsing and managing files. It displays documents from Azure Blob Storage within the File Manager. When a file is selected, the fileSelect event is triggered, and its action is explained in the next step.

Files.component.html

<div class="file-manager-container">
  <div id="file-manager-header">
    Azure blob storage
  </div>
  <ejs-filemanager 
    [ajaxSettings]="ajaxSettings"
    (fileSelect)="onFileSelect($event)"
    height="calc(100vh - 51px)">
  </ejs-filemanager>
</div>

Below is a reference image showing how the File Manager looks.

<alt-text>

Setting up the File Manager

b. Configuring backend communication and handling file selection

The File Manager component is connected to the backend through ajaxSettings, which specifies the service URL for handling document management. When a file is selected, the application:

  • Retrieves the file name from the selected file details.

  • Generates a unique room ID for collaborative editing.

  • Navigates to the Word Document Editor, allowing real-time collaboration.

files.component.ts

  /**
   * onFileSelect: Event handler triggered when a file or folder is selected.
   * - Read 'args.fileDetails' to get info about the selected item.
   * - Generate a random room ID.
   */

  /**
   * ajaxSettings: Configuration for the Syncfusion File Manager to communicate
   * with the server-side endpoints. The 'url' property is used for basic file operations.
   */
  public ajaxSettings: object = {
    url: this.serviceUrl + 'AzureDocumentStorage/ManageDocument',
  };

  /* - Construct a URL for the collaborative editor page, passing the file name and room ID.
   * - Set a flag in DataService to indicate that the author is opening the file.
   * - Finally, navigate to that URL using Angular's router.
   */
  onFileSelect(args: any): void {
    // args.fileDetails is an array; we use the first item.
    if (args?.fileDetails) {
      const selectedFile = args.fileDetails; // The selected file details
      const fileName = selectedFile.name; // Extract the file name
      const roomId = this.generateRandomRoomId(); // Generate a unique room ID
      // Construct the URL for collaborative editing.
      const documentUrl = `/detail/${fileName}/${roomId}`;
      // Indicate that the author is opening the file
      this.dataService.setIsAuthorOpened(true);
      // Navigate to the collaborative editor page.
      this.router.navigateByUrl(documentUrl);
    }
  }

  // Utility method to generate a random room ID.
  generateRandomRoomId(): string {
    return 'RoomID_' + Math.random().toString(32).slice(2, 8);
  }

Step 3: Configure the document editor for collaborative editing

To enable real-time collaborative editing in the Document Editor, the process includes setting up the UI, enabling collaboration, establishing a SignalR connection, handling incoming data, loading documents from the server, and syncing content changes in real time.

a. Setting up the document editor UI

The Document Editor component is added to document-editor.component.html, allowing users to edit documents collaboratively. When a file is selected, the following steps occur:

1. Prompt for username
The application displays a dialog prompting the user to enter their username. This is used to identify each participant during collaborative editing and to track who is making real-time changes to the document.

Note: Since user login and management are not implemented in this demo, the dialog serves as a simple way to capture the username before joining the session. You can remove the logic and link it to the current user in your application.

<alt-text>

Setting up the Document Editor

2. Open the document for collaborative editing
After providing the username, the application opens the document in the Word Document Editor, enabling real-time collaborative editing.

<alt-text>

Document Editor

The following code demonstrates the functionalities described earlier.

document-editor.component.html

<div>
  <div id="dialog-container" *ngIf="showDialog">
    <ejs-dialog 
      id='dialog' 
      #ejDialog 
      target="#dialog-container" 
      [isModal]="true" 
      header='Enter Your Name' 
      [showCloseIcon]='true' 
      [buttons]='dialogButtons' 
      width='400px' 
      (open)="onDialogOpen()">
        <div>
          <input 
            type="text" 
            id="userNameInput" 
            [(ngModel)]="userName" 
            placeholder="Enter your name" 
            class="e-input" />
        </div>
    </ejs-dialog>
  </div>

  <div *ngIf="isUserNameEntered">
    <div 
      id="documenteditor_titlebar" 
      class="e-de-ctn-title" 
      style="height:35px;">
    </div>

    <ejs-documenteditorcontainer 
      #documenteditor_default 
      [enableToolbar]="true" 
      [currentUser]="currentUser"
      [toolbarItems]="toolbarItems"
      (created)="onCreated()"
      (contentChange)="onContentChange($event)" 
      height="calc(100vh - 51px)" 
      style="display: block;">
    </ejs-documenteditorcontainer>
  </div>
</div>

b. UI for sharing the document with other users

To enable document sharing, a share button is added to the title bar. When clicked, a dialog appears displaying the collaborative editing URL. Opening this URL in another tab prompts the user to enter their name (as described in the previous step) before joining the editing session. Once entered, the document opens in the editor, and any changes made in one tab are reflected in real time in the other tab. The document can also be shared with multiple users.

Note: You can customize this document sharing logic by integrating it with your application’s authentication and user management system, allowing users to join the collaborative session without manually entering their names.

<alt-text>

Document sharing

Here is a reference image showing how a document is edited collaboratively by two different users.

<alt-text>

Enabling real-time collaborative document editing

The following code examples demonstrate the functionalities described earlier.

document-editor.component.html

<div id="shareDialog" style="display: none;">
  <div class="e-de-para-dlg-heading">
    Share this URL with others for real-time editing
  </div>
  <div class="e-de-container-row">
    <input type="text" id="share_url" class="e-input" readonly />
  </div>
</div>

title-bar.ts

/**
 * initDialog: Creates a Syncfusion Dialog to share the document URL.
 * This dialog is opened when the user clicks the "Share" button.
 */
private initDialog() {
    this.dialogObj = new Dialog({
        header: 'Share ' + this.documentEditor.documentName,
        animationSettings: { effect: 'None' },
        showCloseIcon: true,
        isModal: true,
        width: '500px',
        visible: false,
        buttons: [{
            click: this.copyURL.bind(this),
            buttonModel: { content: 'Copy URL', isPrimary: true }
        }],
        open: function () {
            // When the dialog opens, select the text in the #share_url input.
            let urlTextBox = document.getElementById("share_url") as HTMLInputElement;
            if (urlTextBox) {
                urlTextBox.value = window.location.href;
                urlTextBox.select();
            }
        },
        beforeOpen: () => {
            // Update the dialog header to reflect the current document name
            if (this.dialogObj) {
                this.dialogObj.header = 'Share ' + this.documentEditor.documentName;
            }
            // Show the #shareDialog container
            let dialogElement: HTMLElement = document.getElementById("shareDialog") as HTMLElement;
            if (dialogElement) {
                dialogElement.style.display = "block";
            }
        },
    });
    // Attach the dialog to an element with ID "shareDialog"
    this.dialogObj.appendTo('#shareDialog');
}

c. Enable collaborative editing in the Document Editor

Implement the backend functionalities for the UI options mentioned above to enable collaborative editing. To begin, inject the CollaborativeEditingHandler into the document-editor.component.ts file and set the enableCollaborativeEditing property to true, enabling real-time collaboration on the document.

document-editor.component.ts

// Enable CollaborativeEditingHandler in DocumentEditor
DocumentEditor.Inject(CollaborativeEditingHandler);

// Enable collaborative editing
this.container.documentEditor.enableCollaborativeEditing = true;

d. Initialize the SignalR connection

Initialize the SignalR connection to manage communication between the client and server. The connection is configured with automatic reconnection, and event listeners are added to handle incoming data, disconnections, and reconnections.

document-editor.component.ts

/**
 * initializeSignalR: Sets up a HubConnection with the specified service URL,
 * configures event listeners and handles reconnection logic.
 */
initializeSignalR = (): void => {
    this.connection = new HubConnectionBuilder().withUrl(this.serviceUrl + 'documenteditorhub', {
        skipNegotiation: true,
        transport: HttpTransportType.WebSockets
    }).withAutomaticReconnect().build();

    // Register event listener for receiving data from SignalR server and handle incoming data from the server
    this.connection.on('dataReceived', this.onDataReceived.bind(this));

    // Handles connection closure
    this.connection.onclose(async () => {
        if (this.connection && this.connection.state === HubConnectionState.Disconnected) {
            alert('Connection lost. Please reload the browser to continue.');
        }
    });

    // Handles reconnection
    this.connection.onreconnected(() => {
        if (this.connection && this.currentRoomName != null) {
            this.connection.send('JoinGroup', { roomName: this.currentRoomName, currentUser: this.currentUser });
        }
    });
}

e. Handle incoming data from the server

The onDataReceived function is triggered when data is received from the server, ensuring real-time synchronization in the document editor. It processes actions like adding or removing users, applies remote edits, and updates the UI accordingly. If the server sends a connectionID, it is stored for tracking, allowing seamless collaboration by keeping all users updated.

document-editor.component.ts

/**
 * onDataReceived: Invoked whenever data arrives from the server (e.g., actions from other users).
 * - If it’s a 'connectionId', store it.
 * - If it’s an 'action' or 'addUser' from a different connection, update TitleBar.
 * - If it’s 'removeUser', also update TitleBar.
 * - Apply the remote action in DocumentEditor.
 */
onDataReceived(action: string, data: any) {
    if (this.container.documentEditor.collaborativeEditingHandlerModule) {
        if (action == 'connectionId') {
            this.connectionId = data;
        } else if (this.connectionId != data.connectionId) {
            if (this.titleBar) {
                if (action == 'action' || action == 'addUser') {
                    this.titleBar.addUser(data);
                } else if (action == 'removeUser') {
                    this.titleBar.removeUser(data);
                }
            }
        }
        // Apply the remote action in the document
        this.container.documentEditor.collaborativeEditingHandlerModule.applyRemoteAction(action, data);
    }
}

f. Load document from server

The loadDocumentFromServer function fetches the document from the server and calls openDocument to load it into the editor.

document-editor.component.ts

/**
 * openDocument: Called after the server responds with the document data (SFDT + version).
 * - Show a spinner while loading.
 * - Update the DocumentEditor with version info and open the SFDT.
 * - Then connect to the room via SignalR.
 * - Hide the spinner.
 */
openDocument(responseText: string, roomName: string): void {
    showSpinner(document.getElementById('container') as HTMLElement);
    let data = JSON.parse(responseText);
    if (this.container) {
        this.container.documentEditor.collaborativeEditingHandlerModule.updateRoomInfo(
            roomName,
            data.version,
            this.serviceUrl + 'api/CollaborativeEditing/'
        );
        this.container.documentEditor.open(data.sfdt);
        setTimeout(() => {
            if (this.container) {
                this.connectToRoom({
                    action: 'connect',
                    roomName: roomName,
                    currentUser: this.currentUser
                });
            }
        });
    }
    hideSpinner(document.getElementById('container') as HTMLElement);
}

/**
 * loadDocumentFromServer: Sends an HTTP request to your server endpoint to retrieve
 * the document SFDT. On success, calls openDocument() with the response.
 */
loadDocumentFromServer() {
    var httpRequest = new XMLHttpRequest();
    httpRequest.open('Post', this.serviceUrl + 'api/CollaborativeEditing/ImportFile', true);
    httpRequest.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
    httpRequest.onreadystatechange = () => {
        if (httpRequest.readyState === 4) {
            if (httpRequest.status === 200 || httpRequest.status === 304) {
                this.openDocument(httpRequest.responseText, this.roomId);
            } else {
                hideSpinner(document.getElementById('container') as HTMLElement);
                alert('Failed to load the document');
            }
        }
    };
    // Send JSON data with file name and room ID
    httpRequest.send(JSON.stringify({ fileName: this.documentName, documentOwner: this.roomId }));
}

/**
 * connectToRoom: After opening the document, establish a SignalR connection
 * and join the specified room for real-time collaboration.
 */
public connectToRoom(data: any) {
    try {
        this.currentRoomName = data.roomName;
        if (this.connection) {
            this.connection.start().then(() => {
                if (this.connection) {
                    this.connection.send('JoinGroup', {
                        roomName: data.roomName,
                        currentUser: data.currentUser
                    });
                }
            });
        }
    } catch (error) {
        console.error(error);
        // Retry connection after 5 seconds
        setTimeout(this.connectToRoom, 5000);
    }
}

g. Handle document content changes

The onContentChange event in the document editor triggers whenever a user modifies the document. The changes are captured and sent to the server, which broadcasts them to all users, ensuring real-time synchronization for seamless collaborative editing.

document-editor.component.ts

/**
 * onContentChange: Called whenever the local user changes the document content.
 * We forward these changes to the server for real-time collaboration.
 */
onContentChange = (args: ContainerContentChangeEventArgs) => {
    this.container.documentEditor.collaborativeEditingHandlerModule.sendActionToServer(
        args.operations as Operation[]
    );
};

Running the projects

To test the functionalities, start the server-side (ASP.NET Core Web API) first, followed by the client-side (Angular) application. The output showcasing collaborative editing by two different users simultaneously on the client side.

<alt-text>

Collaborative editing by two different users simultaneously

GitHub reference

For the complete project, refer to the GitHub demo.

Conclusion

Thanks for reading! In this blog, we explored how to build a real-time collaborative editing solution using Syncfusion’s Document Editor and File Manager integrated with Azure Blob Storage and SignalR, leveraging Redis cache for improved performance. By following the step-by-step guide, you can create a seamless, multi-user editing experience where documents are browsed, opened, and collaboratively edited directly from Azure. This solution ensures a smooth and synchronized editing workflow, making document collaboration efficient, user-friendly, and scalable.

Try implementing these features in your project to enhance real-time document collaboration. The latest version is available for current customers to download from the license and downloads page. If you are not a Syncfusion® customer, try our 30-day free trial to explore the newest features.

You can also contact us through our support forums, support portal, or feedback portal. We are always happy to assist you!

0
Subscribe to my newsletter

Read articles from syncfusion directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

syncfusion
syncfusion

Syncfusion provides third-party UI components for React, Vue, Angular, JavaScript, Blazor, .NET MAUI, ASP.NET MVC, Core, WinForms, WPF, UWP and Xamarin.