Favour Vivian Woka I'm a frontend developer based in Port Harcourt, Nigeria. I am passionate about building applications that run on the web. Technical writing is another interest of mine.

Build a Google Doc clone with HTML, CSS, and JavaScript

12 min read 3593

The CSS, HTML, and JavaScript logos.

Google Docs is Google’s browser-based word processor that allows you to create, edit, download, and share documents online and access it from any computer as long as there is an internet connection.

In this tutorial, we will be looking at how we can build a version of Google Docs using HTML, CSS, and JavaScript with no framework. In order to achieve this, we will be working with Firebase Cloud Firestore, a flexible, easy-to-use database for mobile, web, and server development.

If you’d like to take a peek at the full code for this tutorial before we begin, you can find it in the GitHub repo here.

Structure a Firestore Database

Let’s first look at how to structure a Cloud Firestore Database. After creating a Firebase project and adding a Firebase app to the project, navigate to Cloud Firestore at the sidebar of the dashboard. Then, structure the database just like the image below:

Our Firebase datastore.

The documents within our Firestore datastore.

What we just did is called Firestore data modeling.

We created a top-level collection called Docs. Docs is the collection that contains all the user’s documents.

The next thing is the unique identifier of the doc. The unique identifier won’t be a random identifier — it will be a generated id that is retrieved from an authenticated user because we will be implementing Google authentication on this project.

We also created a sub-collection called documents. Documents is a sub-collection that contains the documents of an authenticated user.

We made a custom demo for .
No really. Click here to check it out.

Lastly is a unique identifier for the document. Under this section, we have fields like:

  • name – the name of the user
  • content – contents of the document
  • created – timestamp of when the document was created
  • updated – timestamp of when the document was last updated

Now that we have structured our cloud Firestore database, let’s take things a step further to enable Firebase Google authentication in our Firebase project.

To achieve this, navigate to the authentication tab at the sidebar of the dashboard and click on the sign-in method tab. Next, click on the Google section. A popup dialog will appear. Click on the Enable button and click Save to save the changes.

Project setup

Go to your project folder and create a signup.html file, a signup.css file, a firebase.js, and a code.auth.js file.

First, let’s add Firebase to our project. Go to the Firebase Google console, copy the project config, and past the code inside your firebase.js file just like the code below:

const config = {
        apiKey: 'project key',
        authDomain: 'project.firebaseapp.com',
        databaseURL: 'https://project.firebaseio.com',
        projectId: 'project',
        storageBucket: 'project.appspot.com',
        messagingSenderId: 'projectSenderId',
        appId: 'ProjectId'
};
firebase.initializeApp(config);

Add Google authenticating

Inside the signup.html file, write the following code:

<div class="google">
<button class="btn-google" id="sign-in">
        <img src="image/Google.png" width="24" alt="" />
         <span>Sign up with Google</span>
    </button>
</div>

In the above code, we have a div with a class google and a button with a class btn-google. We also have an id sign-up.

The div is the container that wrapped the auth button.

Let’s implement the Google auth function. Inside the auth.js file, copy and paste the code below:

function authenticateWithGoogle() {
    const provider = new firebase.auth.GoogleAuthProvider();
  firebase
  .auth()
  .signInWithPopup(provider)
  .then(function (result) {
          window.location.href = '../index.html';
  })
  .catch(function (error) {
      const errorCode = error.code;
      const errorMessage = error.message;
      const email = error.email;
      const credential = error.credential;
      console.log(errorCode, errorMessage, email,   credential);
  });
}

In the above code, we created a function called Google authentication(). The function triggers the Firebase Google popup when a user clicks on the Google sign-up button on the project web page. If the sign-up is successful, the developer console will log in the user and close the popup.

Now that our Google sign-up has been implemented, let’s move on to the next task.

Creating our text editor

We will be creating a basic text editor where we can type in a word and edit it. To do that, let’s start by creating an editor.html file and write the code below:

<div class="edit-content">
  <p class="loading" id="loading">Saving document....</p>
  <div class="editor" contenteditable="true" id="editor"></div>
</div>

In the above code, we create a div and bind an attribute contenteditable and set the value to true. The contenteditable attribute turns any container into an editable text field.

If we look at our web page, we can see the div has turned into an editable text field. The next thing to do is implement text formatting features such as italics, bold, text align, etc.

Implement text format

Bold

The first text format we are going to implement is bold. Let’s look at the code below:

<a href="javascript:void(0)" onclick="format('bold')">
  <span class="fa fa-bold fa-fw"></span>
</a>

The code above is a built-in JavaScript function that takes an appropriate value and formats it when the function is called.

Italic

<a href="javascript:void(0)" onclick="format('italic')">
  <span class="fa fa-italic fa-fw"></span>
</a>

The italic function italicizes text. Whenever text is being highlighted, the function is fired — even when the text is not highlighted, as long as the function is fired.

Unordered list and ordered list

<a href="javascript:void(0)" onclick="format('insertunorderedlist')">
  <span class="fa fa-list fa-fw"></span>
</a>
<a href="javascript:void(0)" onclick="format('insertOrderedList')">
  <span class="fa fa-list-ol fa-fw"></span>
</a>

The unordered list function adds bullet points to a text, and the ordered list function adds numbers to a text.

Justify left, justify full, justify center and justify right

<a href="javascript:void(0)" onclick="format('justifyLeft')">
  <span class="fa fa-align-left fa-fw"></span>
</a>
          
<a href="javascript:void(0)" onclick="format('justifyFull')">
  <span class="fa fa-align-justify fa-fw"></span>
</a>
          
<a href="javascript:void(0)" onclick="format('justifyCenter')">  
<span class="fa fa-align-center fa-fw"></span>
</a>

<a href="javascript:void(0)" onclick="format('justifyRight')">
  <span class="fa fa-align-right fa-fw"></span>
</a>

From the name of the function, we can tell that the Justify Left function aligns text to the left. By default, all text is aligned to the left, so we might not notice the changes.

The Justify Full function justifies a text, and the Justify Center and Justify Right functions center a text to the center and justifies a text to the right, respectively.

Underline

<a href="javascript:void(0)" onclick="format('underline')">
  <span class="fa fa-underline fa-fw"></span>
</a>

The underline function underlines a text when the function is fired.

Choose color, change font size, and select font

<input class="color-apply" type="color" onchange="chooseColor()"
id="myColor"/>
<select id="input-font" class="input" onchange="changeFont (this);">
    <option value="Arial">Arial</option>
    <option value="Helvetica">Helvetica</option>
    <option value="Times New Roman">Times New Roman</option>
    <option value="Sans serif">Sans serif</option>
    <option value="Courier New">Courier New</option>
    <option value="Verdana">Verdana</option>
    <option value="Georgia">Georgia</option>
    <option value="Palatino">Palatino</option>
    <option value="Garamond">Garamond</option>
    <option value="Comic Sans MS">Comic Sans MS</option>
    <option value="Arial Black">Arial Black</option>
    <option value="Tahoma">Tahoma</option>
    <option value="Comic Sans MS">Comic Sans MS</option>
</select>
<select id="fontSize" onclick="changeSize()">
    <option value="1">1</option>
    <option value="2">2</option>
    <option value="3">3</option>
    <option value="4">4</option>
    <option value="5">5</option>
    <option value="6">6</option>
    <option value="7">7</option>
    <option value="8">8</option>
</select>

The Change Font size is a select drop-down that displays different font sizes and takes the value of the selected size. It applies this to the highlighted text, and the Select Font function is a select drop-down that shows different fonts. It takes the selected font value and applies it to the highlighted text.

If the above format setup is implemented, we should have something that looks like this;

Our doc with text formatting options.

In the image above, we can see the editor field and the toolbar that has the different text formatting options.

At this point, if we type and implement the different text formats on the text, we will notice nothing happens. The reason for this is that we have not yet implemented the JavaScript in-built function, or the helper function that gets the document and the command and applies the command to the document.

Implementing the helper function

Create a main.js file and write the code below:

function format(command, value) { 
  document.execCommand(command, false, value);
}

The function Format runs each time a text format is clicked. The function takes in two arguments: a command and a value. The command is the name of the text format that was fired, and the value is the text that was highlighted.

The document.execCommand only returns true if it is invoked as part of a user interaction.

Implement the helper function for change font size and select font

function changeFont() {
  const Font = document.getElementById('input-font').value;
  document.execCommand('fontName', false, Font);
}

function changeSize() {
  const size = document.getElementById('fontSize').value;
  document.execCommand('fontSize', false, size);
}

The first helper function is the changeFont function. The function runs when the change font format is fired. It takes the selected font and applies it to the highlighted text.

The second function is the changeSize function. It works the same as the changeFont function, but the difference is that it changes the font size of the highlighted text.

If we type a text and apply any of the formatting options, we should be able to see the formatting applied to the text that was highlighted.

Now we have implemented the text editor and some text format. The next thing we are going to look at is how we can save the document to the Firebase Cloud Firestore Database that we structured.

Save a user document to Cloud Firestore

Let’s look out how we can save documents to Firestore when a user creates a document. You’ll recall that when when creating the editable div with the contenteditable attribute, we gave it an id attribute. We are going to listen to the editable div and get the value as the user is creating a document using the id attribute.

First, we are going to check if the user is authorized or not. If the user is authorized, we get the id of the user and assign it to a variable inside the the main.js folder.

let userId = '';
let userName = '';
firebase.auth().onAuthStateChanged(function(user) {
  if (user) {
    userId = user.uid;
    userName = user.displayName;
    init();
  } else {
    console.log(user + '' + 'logged out');
  }
});

function init(){
  const token = localStorage.getItem('token');
  if(!token){
    const docId = firebase.firestore().collection('docs')
                                      .doc(userId)
                                      .collection('documents')
                                      .doc().id;
      localStorage.setItem('token', docId);
    }else{
        delay(function(){
          getSingleDocDetails(token);
        }, 1000 );
    }
}

The firebase.auth().onAuthStateChanged function is a Firebase function that checks if the user is logged in or not. If the user exists, we get a user.id and assign the id to a variable called userId that was created above.

The init() function checks if there is a document id that is stored inside localStorage. If there is not, it creates a doc id from Firestore and sets it inside localStorage. If there is, it calls the getSingleDocDetails() function. But we are going to look at the getSingleDocDetails() function later on.

Let’s look at how we can get user documents and save them.

Inside the the main.js folder, write the code below:

const editor = document.getElementById('editor');
let dos = '';

editor.addEventListener('input', e => {
  dos = e.target.innerHTML;
  delay(function(){
    addDoc(word);
  }, 1000 );
});

var delay = (function(){
  var timer = 0;
  return function(callback, ms){
    clearTimeout (timer);
    timer = setTimeout(callback, ms);
  };
})();

We created a variable called editor. We assign the value to be the div with the contenteditable attribute using the id attribute we assigned.

document.getElementById searches for an HTML tag with the id name that was passed to it.

Next, we listened to the div to find out when the user has started typing by calling an event listener editor.addEventListener(input, (e)).

The .addEventListener(input, (e)) event listens to any change made inside the editable filed. Once the change is made, we targeted the div innerHtml and passed the value as a parameter to a function.

Note, we used .innerHTML, and not .value because we are working with a div.

We also called the delay() function. The delay() is a function that stops the addDoc() function so that it waits until a user finishes typing before saving the data to cloud Firestore.

Calling the addDoc() function

function addDoc(word) {

  const docId = localStorage.getItem('token');

    firebase
    .firestore()
    .collection('docs').doc(userId)
    .collection('documents').doc(docId).set({
      name: userName,
      createdAt: new Date(),
      updated: new Date(),
      content: word,
    })
    .then(() => {
      loading.style.display = 'none';
    })
    .catch(function(error) {
      console.error('Error writing document: ', error);
    });
}

Inside the addDoc() function, we first get the id that we created from the local storage. The next thing we do is call the Firebase query function .set() and pass the current logged-in user’s uid as an argument at the first .doc() method, and also the docId that was created as an argument at the second .doc().

We set the name of the document as the userName of the currently logged-in user. This is then created as a new Date() object. Then, it’s updated as the new Date object. Lastly, the content is updated as the document that was created by the user.

If we check the Firestore database, we will see the document saved.

The next thing we are going to look at is how we can retrieve our data from Cloud Firestore.

Get user’s document from Cloud Firestore

We are going to implement the dashboard page before fetching the users document. Write the code below:

<nav class="navbar">
      <div class="nav-col-logo">
        <a href="#"><i class="fa fa-book"></i> GDocs</a>
      </div>
      <div class="nav-col-input">
        <form id="searchForm">
          <input type="search" placeholder="Search" id="search" />
        </form>
      </div>
      <div class="nav-col-img">
        <a href="#"><i class="fa fa-user"></i></a>
      </div>
    </nav>
    <div class="documents">
      <div class="section group">
        <div class="col span_1_of_3"><h4>Today</h4></div>
        <div class="col span_1_of_3"><h4>Owned by anyone</h4></div>
        <div class="col span_1_of_3"><h4>Last opened</h4></div>
      </div>
      <div id="documents"></div>
    </div>
    <div class="creat-new-doc">
      <button class="btn-color" type="button" id="createNewDoc">
        +
      </button>
    </div>

If the above code has been implemented, we should have something like the picture below in our web page:

Our dashboard page in Cloud Firestore.

In the above image, we can see the button with a blue background. The button takes the user to the editor page, where a user can create a new document. The default data above shows how the document layout will be displayed after we fetch the document that was created by a user and save it to Cloud firebase.

Getting the actual data

Write the code below:

let holdDoc = [];
function getDocuments(id) {
  // eslint-disable-next-line no-undef
  let db = firebase.firestore()
    .collection('docs')
    .doc(id)
    .collection('documents');
    db.get()
    .then((querySnapshot) => {
      querySnapshot.forEach(function(doc) {
        let dcus = doc.data();
        dcus.id = doc.id;
        holdDoc.push(dcus);
        showDoc();
      });
    });
}

We created a function getDocument(). Inside the function, we queried the Firebase Firestore using the .get() method. We loop that through the object we got, and push it to an empty array we created as well as the doc id. Then, we called the showDoc() function that displays the actual data.

Now, let’s display the actual data:

const docBook = document.getElementById('documents');
function showDoc() {
  docBook.innerHTML = null;
  for (let i = 0; i < holdDoc.length; i++){
    let date = new Date( holdDoc[i].updated.toMillis());
    let hour = date.getHours();
    let sec = date.getSeconds();
    let minutes = date.getMinutes();
    var ampm = hour >= 12 ? 'pm' : 'am';
    hour = hour % 12;
    hour = hour ? hour : 12;
    var strTime = hour + ':' + minutes + ':' + sec + ' ' + ampm;
    let subString = holdDoc[i].content.replace(/^(.{14}[^\s]*).*/, '$1');
    docBook.innerHTML += `
      <div class="section group">
        <div class="col span_1_of_3">
          <p><a id="${holdDoc[i].id}" onclick="getSingleDocId(id)">
            <i class="fa fa-book"></i> 
              ${subString}  
            <i class="fa fa-users"></i>
          </a></p>
        </div>
        <div class="col span_1_of_3">
          <p>${holdDoc[i].name}</p>
        </div>
        <div class="col span_1_of_3">
          <div class="dropdown">
            <p> ${strTime} 
              <i class="fa fa-ellipsis-v dropbtn" 
                onclick="myFunction()" >
              </i>
            </p>
            <div id="myDropdown" class="dropdown-content">
              <a href="#" target="_blank" >Delete Doc</a>
              <a href="#">Open in New Tab</a>
            </div>
          </div>
        </div>
      </div>
       `;
  }
}

We first get the id of the div where we want to display the document. After that, we called the showDoc() function. Inside the showDoc() function, we first loop through the object we got and then we append it to the variable that we created. If we load the web page, we can see the data being displayed.

Another thing we will be looking at is how to update the document, or edit the document:

function getSingleDocId(id){
  console.log(id);
  localStorage.setItem('token', id);
  window.location.href = '../editor.html';
}

If we look at the showDoc() function that we wrote, we can see that we pass the id of the document inside a function as a parameter. Then we called the function outside. Inside the function, we get the id and store it inside localStorage. Then, we can navigate the user to the editor page.

Inside the editor.js page, write the code below:

function getSingleDocDetails(docId){
  firebase
    .firestore()
    .collection('docs')
    .doc(userId)
    .collection('documents')
    .doc(docId)
    .get()
    .then((doc) => {
      if (doc.exists) {
        editor.innerHTML += doc.data().content;
      } else {
        console.log('No such document!');
      }
    }).catch(function(error) {
      console.log('Error getting document:', error);
    });
}

Inside the editor page, we define an init() function that checks if there is an id stored in the localStorage. If there is, it calls the getSignleDocDetails() function and fetches the document from cloud Firestore and displays it for the user to continue.

If the user make any changes, it will update the document.

Online and offline editing

Let’s look at how we can implement online and offline editing. We want to still be able to save the user’s document if the user goes offline, and be able to sync it to Firebase if your user comes back online without interrupting the user. To achieve this, write the code below:

function updateOnlineStatus() {
  }
updateOnlineStatus();

In the above code, we first created a function called updateOnlineStatus() and called the function outside the scope. Now we are going to copy the method that gets the users’ document and pastes it inside the function, just as the code below:

function updateOnlineStatus() {
  editor.addEventListener('input', e => {
      dos = e.target.innerHTML;
      delay(function(){
        addDoc(dos);
      }, 1000 );
    });
  }

After that, we are going to listen to the browsers to track when the user is online and offline. Write the code below:

editor.addEventListener('input', e => {
      dos = e.target.innerHTML;
      delay(function(){
        addDoc(dos);
      }, 1000 );
   if (navigator.onLine === true) {
      const word =  localStorage.getItem('document');
      addDoc(word);
      localStorage.removeItem('document');
      
      return;
    } else {
      localStorage.setItem('document', dos);
      
      return;
    }
      
});

We used an if statement to check if the navigator.online === true. The navigator.online is a property that returns a true or false value. The true value returns when a user is online, and the false value returns when a user is offline.

We set the condition to check if the user is online while editing or creating a document. If the user is online, we get the document from local storage and send it to cloud Firestore, but if the user is offline, continue to save the document to local storage.

Conclusion

In this article, we learned how to create a basic text editor. Also, we were able to understand how to structure a cloud Firestore database, how to use the Firebase .set() method, and how to integrate online and offline editing. You can find the full code for this tutorial on GitHub.

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

Is your frontend hogging your users' CPU?

As web frontends get increasingly complex, resource-greedy features demand more and more from the browser. If you’re interested in monitoring and tracking client-side CPU usage, memory usage, and more for all of your users in production, try LogRocket.https://logrocket.com/signup/

LogRocket is like a DVR for web apps, recording everything that happens in your web app or site. Instead of guessing why problems happen, you can aggregate and report on key frontend performance metrics, replay user sessions along with application state, log network requests, and automatically surface all errors.

Modernize how you debug web apps — .

: Debug JavaScript errors more easily by understanding the context

Debugging code is always a tedious task. But the more you understand your errors the easier it is to fix them.

LogRocket allows you to understand these errors in new and unique ways. Our frontend monitoring solution tracks user engagement with your JavaScript frontends to give you the ability to find out exactly what the user did that led to an error.

LogRocket records console logs, page load times, stacktraces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!

.
.
Favour Vivian Woka I'm a frontend developer based in Port Harcourt, Nigeria. I am passionate about building applications that run on the web. Technical writing is another interest of mine.

9 Replies to “Build a Google Doc clone with HTML, CSS, and…”

  1. Hello Vivian!
    To use firebase firestore do we need a blaze (pay as you go) account?? Some time ago I couln’t deploy my app because firebase says that I can only deploy if I change my free plan to blaze.

    P.D. I don’t change to blaze because I don’t have a credit/debit card

  2. You don’t need to be on Blaze plan to make use of firebase unless you are working with cloud functions or some other feature that requires a change in plan.

  3. Nice job 🙂 but I would like to see the full code.. Do you have a link where I can find it?

  4. Great example!!! Thanks a lot…
    Any tips of Firebase data model for sharing documents with different users fr viewing, editing?

  5. can u please tell me the linking of all these codes . And also where to put the code “getting actual data ” code and others afterward.

Leave a Reply