Build your own Google Docs like App

Several customers have approached us and asked how they can create a Google Docs like web app using our APIs. Google Docs is a word processor that allows users to create and edit files online while collaborating with other users in real-time.

This blog post explains how easy it is to create a lite version of Google Docs that has the following features:

  • Rich Text Editing (change text font, size, colour, style (boldface, italic), alignment etc.).
  • Real-time collaborative editing of the same document. Multiple users can access the document at the same time and modify it.
  • Upload content of an existing Word Document into an editor.
  • Save text in the editor as MS Word, PDF, TXT or HTML document.

Our end product will look as follows:

Google Docs like App Interface

Tools and Technologies – Create Google Docs like App

We will develop the Google Docs like web app in ASP.NET Core and use the following two libraries:

  • Firepad is an open-source, collaborative text editor. It uses the Firebase Realtime Database as a backend so it requires no server-side code and can be added to any web app simply by including the JavaScript files.
  • GroupDocs.Editor for .NET gives us an ability to edit most popular document formats using any WYSIWYG editor without any additional applications. We will load document via GroupDocs.Editor into Firepad, edit document in a way we want and save it back to original document format.

I have used Visual Studio for Mac as an IDE. However, you can download the free community edition of Visual Studio, depending upon your platform, from here. Let’s start.

Create a new ASP.NET Core Web Application project and name the project “GoogleDocsLite”.

Create a new ASP.NET Core Web App

Run the application to ensure everything is set up properly.

Integrate Firepad

We can add Firepad to our web app by including the following JavaScript files in the <head> section of _Layout.cshtml.

<!-- Firebase -->
<script src="https://www.gstatic.com/firebasejs/7.13.2/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/7.13.2/firebase-auth.js"></script>
<script src="https://www.gstatic.com/firebasejs/7.13.2/firebase-database.js"></script>

<!-- CodeMirror -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.17.0/codemirror.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.17.0/codemirror.css" />

<!-- Firepad -->
<link rel="stylesheet" href="https://firepad.io/releases/v1.5.9/firepad.css" />
<script src="https://firepad.io/releases/v1.5.9/firepad.min.js"></script>

<!-- userlist -->
<script src="~/js/firepad-userlist.js"></script>
<link rel="stylesheet" href="~/css/firepad-userlist.css" />

To create a Firepad, we will initialize Firebase, CodeMirror and then Firepad. Add the following script and HTML code in Index.cshtml.

<script>
    function init() {
        // Initialize Firebase.
        // TODO: replace with your Firebase project configuration.
        var config = {
            apiKey: '',
            authDomain: "",
            databaseURL: ""
        };
        firebase.initializeApp(config);
        
        // Get Firebase Database reference.
        var firepadRef = getExampleRef();
        
        // Create CodeMirror (with lineWrapping on).
        var codeMirror = CodeMirror(document.getElementById('firepad'), { lineWrapping: true });

        // Create a random ID to use as our user ID (we must give this to firepad and FirepadUserList).
        var userId = Math.floor(Math.random() * 9999999999).toString();

        // Create Firepad (with rich text features and our desired userId).
        var firepad = Firepad.fromCodeMirror(firepadRef, codeMirror,
                    { richTextToolbar: true, richTextShortcuts: true, userId: userId });

        // Create FirepadUserList (with our desired userId).
        var firepadUserList = FirepadUserList.fromDiv(firepadRef.child('users'),
        document.getElementById('userlist'), userId);
    }
    
    // Helper to get hash from end of URL or generate a random one.
    function getExampleRef() {
        var ref = firebase.database().ref();
        var hash = window.location.hash.replace(/#/g, '');
        if (hash) {
            ref = ref.child(hash);
        } else {
            ref = ref.push(); // generate unique location.
            window.location = window.location + '#' + ref.key; // add it as a hash to the URL.
        }
        if (typeof console !== 'undefined') {
            console.log('Firebase data: ', ref.toString());
        }
        return ref;
    }
</script>

<div id="userlist"></div>
<div id="firepad"></div>

Please replace the contents of config with your own Firebase project’s config.

We want the above script to execute once a web page has completely loaded all content (script files, CSS files etc.). So, call init() function from the onLoad event attribute of <body> element in _Layout.cshtml.

<body onload="init()">

Your <body> element should look as follows. If it contains unnecessary tags like <header>, <footer>, please remove them.

Body Element

If you run the project, you will notice that firepad and userlist are not aligned properly. Please use the following CSS code to adjust the size/position of the firepad and userlist. You may add the following code within <head> element of _Layout.cshtml.

<style>
    html {
        height: 100%;
    }

    body {
        margin: 0;
        height: 100%;
    }

    /* We make the user list 175px and firepad fill the rest of the page. */
    #userlist {
        position: absolute;
        left: 0;
        top: 50px;
        bottom: 0;
        height: auto;
        width: 175px;
    }

    #firepad {
        position: absolute;
        left: 175px;
        top: 50px;
        bottom: 0;
        right: 0;
        height: auto;
    }
</style>

Firepad has been successfully set up.

Upload Content of an Existing Word Document into an Editor

Now we want to give our users a way to upload content of an exiting Word Document in the text editor. On the frontend, we add an <input> element of type file that lets a user select a Word Document from their local machine. On the backend, we use GroupDocs.Editor library to retrieve the content of a Word Document as HTML string. Finally, we use Firepad’s setHtml() method to show content in the text editor.

Add the following <form> element in Index.cshtml file before <div id=”userlist”> tag.

<form method="post" enctype="multipart/form-data" id="uploadForm">
    <input asp-for="UploadedDocument" />
    <input type="submit" value="Upload Document" class="btn btn-primary" asp-page-handler="UploadDocument" />
</form>

In Index.cshtml.cs file, define a corresponding property.

[BindProperty]
public IFormFile UploadedDocument { get;  set; }

Run the project and click on the Choose file button. Select a Word Document that you want to upload and click on the Upload Document button. Nothing will happen because we have not yet defined the handler in Index.cshtml.cs. Before we do this, let’s first add GroupDocs.Editor library in our project.

Integrate GroupDocs.Editor

GroupDocs.Editor is available as a NuGet package so we can easily add it to our project. Right-click on the project and select Manage NuGet Packages option. The Manage NuGet Packages window will open, select the Browse tab, and enter GroupDocs.Editor in the search field. GroupDocs.Editor should appear as a first result, select it and then click on Add Package button.

Add GroupDocs.Editor via NuGet Package Manager

When the package is successfully added, it will appear under NuGet subfolder in the Dependencies folder.

Form Data Handling

Now we write a handler (OnPostUploadDocument() method) that will be called when a user clicks on Upload Document button. UploadedDocument object (of type IFormFile) contains content of the uploaded document. First, we save the document on the server then use GroupDocs.Editor library to get its content as HTML string. Please add the following code in Index.cshtml.cs file.

private readonly IWebHostEnvironment _hostingEnvironment;

public string DocumentContent { get; set; }

public IndexModel(IWebHostEnvironment hostingEnvironment)
{
    _hostingEnvironment = hostingEnvironment;
}

public void OnPostUploadDocument()
{
    var projectRootPath = Path.Combine(_hostingEnvironment.ContentRootPath, "UploadedDocuments");
    var filePath = Path.Combine(projectRootPath, UploadedDocument.FileName);
    UploadedDocument.CopyTo(new FileStream(filePath, FileMode.Create));
    ShowDocumentContentInTextEditor(filePath);
}

private void ShowDocumentContentInTextEditor(string filePath)
{
    WordProcessingLoadOptions loadOptions = new WordProcessingLoadOptions();
    Editor editor = new Editor(filePath, delegate { return loadOptions; }); //passing path and load options (via delegate) to the constructor
    EditableDocument document = editor.Edit(new WordProcessingEditOptions()); //opening document for editing with format-specific edit options

    DocumentContent = document.GetContent();
}

Firepad provides two events for listening. One of them is ‘ready‘ which fires once Firepad has retrieved the initial editor contents. We attach a callback to this event type and in the callback, we pass DocumentContent string as an argument to setHtml() method of firepad object. Please add the following code in the init() function in Index.cshtml.

firepad.on('ready', function () {
    if (firepad.isHistoryEmpty()) {
        var documentContent = '@Model.DocumentContent';
        if (documentContent.length != 0) {   
            firepad.setHtml(htmlDecode(documentContent));
        } else {
            firepad.setText("Welcome to your own private pad! Share the URL above and collaborate with your friends.");
        }
    }
});

You may have noticed that we passed the documentContent string first to htmlDecode() method before passing to setHtml() method. It is to replace character entities such as &lt, &gt with signs (< and >). The htmlDecode() method looks as follows.

function htmlDecode(input) {
    var e = document.createElement('div');
    e.innerHTML = input;
    return e.childNodes[0].nodeValue;
}

Run the project, now you should be able to upload the content of a Word Document into an editor.

In part II of this post, I have explained how we can extend our application to include the following features:

  • Download content of the editor as MS Word, PDF, TXT or HTML document.
  • Share the URL with friends so that they can edit the document at the same time.

Please check it out.

Complete source code of the project is available on GitHub.

See Also