FireEdit: Build a Real-time Editor with JavaScript & Firebase
I've always been excited about real-time applications on the web. A few years ago,
my first attempts consisted of implementing recurring requests, which would eventually
fetch information from the server simulating real-time behavior. It worked, but
it wasn't efficient. Since then, I've learned about web sockets.
I heard developers talking about something called Firebase. Recently,
Emma β a good friend, fellow programmer, and amazing human being β needed help with Firebase, and we decided to learn it together.
Now, we are not experts, but since we are good enough at it, we thought we'd show you how to build an app using Firebase and JavaScript β specifically, a real-time text editor which lives in your
browser.
TL;DR
This is our final result: FireEdit β π Use it, π star it, π΄ fork it and enjoy! π
What We Did: π₯ FireEdit π
Collaboration is important everywhere. Emma and I often share code snippets (especially when we need a quick fix to a bug). However, sharing little code snippets via chat apps isn't the smoothest experience β they often lack edit functionality, syntax highlighting, monospace fonts etc.
To simplify the process, I came up with the idea to build a Firebase powered editor, where we can share code while learning Firebase (kind of a circular experience,
isn't it?).
We called our app: FireEdit
(we used Firebase to create such an editor, hence the name). This is the final result: https://coltaemanuela.github.io/FireEdit/
(you will notice a lovely spinner while the editor is being loaded)
Oh, yes, I know there are existing solutions that are better than what we have here, but hey, we actually did this as our first-hand experience, it's open-source (you can do almost anything with the code we have written), and we're going to show you how we did it!
From this point on, this technical post is going to show you how you can build a similar editor.
So, What Exactly is Firebase?
Firebase is a mobile (iOS, Android) and web application platform that provides various tools and an infrastructure aimed to support developers in building real-time applications. Since we're talking about web applications, we will forget it's
mobile friendly too.
When you're just starting to work with Firebase, the first step you have to complete is to sign up for it. Then, you can start creating and managing your databases and integrate Firebase in your application.
It is important to know that the data you store in the database
is structured as JSON objects.
A quick guide on how to get started with Firebase is available here. We'll also very quickly explain what you have to know about it.
One of the greatest features in Firebase is the series of powerful tools that simplify authentication system (email & password authentication or third-party providers auth), files storing, and real-time synchronization. We will focus on the real-time synchronization since we want to build a real-time editor.
There are a few limitations if you opt for the free plan, but they are still pretty liberal. It shouldn't matter too much β if you do need super-powers, you can always upgrade to a paid plan.
Ace Editor
Imagine a text editor (like Sublime Text) which actually works in your browser (without installing any additional software, except the browser itself β which you already have, since you are reading this very text).
That's what the ACE Editor is: a fancy text editor in your browser (it comes with syntax highlighting, themes, optional VIM/Emacs bindingsβyay! ). Check out the official demos β by the way, they have great documentation too!
There are so other alternatives as well. You can find them in a good and detailed list on Wikipedia.
Creating the App
It's going to be a web app. Therefore, we will need a little bit of HTML, CSS and of course, JavaScript! Let's create these files following this structure:
βββ css
β βββ style.css
βββ index.html
βββ js
βββ index.js
index.html
is the file that is going to load css/style.css
and js/index.js
once it's opened in the browser.
Let's start with a minimal HTML code in index.html
:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>FireEdit</title>
<link rel="stylesheet" href="css/style.css" type="text/css" media="screen" charset="utf-8">
</head>
<body>
<h2>Welcome to FireEdit!</h2>
<div id="editor"></div>
<!-- Firebase -->
<script src="https://www.gstatic.com/firebasejs/3.6.4/firebase.js"></script>
<!-- jQuery -->
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<!-- Ace Editorβkeep reading, and you'll see how we're going to use this -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ace.js" type="text/javascript" charset="utf-8"></script>
<!-- Load the main JavaScript file from our app. -->
<script type="text/javascript" src="js/index.js"></script>
</body>
</html>
We will not focus on the CSS part (which takes care of the styling) for now. Instead, we'll focus on the functionality part (the JavaScript file).
One of the first things we have to do is to create a Firebase application.
Firebase Configuration
-
Create a new Firebase app
-
Load the Firebase script in your app:
<script src="https://www.gstatic.com/firebasejs/3.6.4/firebase.js"></script>
We already have that in the HTML code above. As newer versions of Firebase are released, you'll have to change the version (
3.6.4
) in the URL. -
Set the configuration (you can find this by clicking the Add Firebase to your web app button in the Overview section)
// Initialize Firebase var config = { apiKey: "AI...kBY", authDomain: "... .firebaseapp.com", databaseURL: "https://... .firebaseio.com", storageBucket: "... .appspot.com", messagingSenderId: "9...6" }; firebase.initializeApp(config);
Quick Introduction for Using Firebase in JavaScript
Imagine the Firebase database like a big object where you can store data. We designed the schema like below (this is an example how an object can look like):
{
"root": {
"editor_values": {
"johnny+emma+article": {
"content": "Hello world. This is how we started this very article. :D",
"lang": "markdown",
"queue": {...}
}
}
}
}
So, how can we interact with such a structure form JavaScript? First we need the reference to the root
node. firebase.database()
is going to return that:
// Get the database object
var db = firebase.database();
After we have the root, we can get specific data. Let's assume we want the content of the editor with id johnny+emma+article
:
// We know what's the editor id
var editorId = "johnny+emma+article";
// Get the reference to the editor values
var editorValues = db.ref("editor_values");
// Get the entire editor object
editorValues.child(editorId).once("value", function (snapshot) {
console.log(snapshot.value());
/* {
"content": "Hello world. This is how we started this very article. :D",
"lang": "markdown",
"queue": {...}
} */
});
// Get the value of the `content` field only:
editorValues.child(editorId).child("content").once("value", function (snapshot) {
console.log(snapshot.value());
// "Hello world. This is how we started this very article. :D"
});
Using the set
or update
methods we can write data in the Firebase database.
For instance, let's change the content:
// This is going to set the content in the editor to "hello world"
editorValues.child(editorId).update({
content: "hello world"
});
Syncing the content with other clients
When we make an update in the database, that update event is propagated to all
the clients listening to it. For example, when I change the data in my editor, I want Emma to see what I've changed.
We started with a simple <textarea>
element to have a proof of concept.
In pseudo-code that would look like this:
[textarea]
onchange -> save the value in Firebase
Firebase:
- on change: update the textarea value
We got it working, but we quickly realized that this is not going to work because, on each update, the cursor would jump at the end of the textarea. We needed something more powerful: a real editor. This is when we decided on the ACE editor. First, let's initialize the editor.
var editor;
...
// Initialize the editor
editor = ace.edit("editor");
// Set the editor theme
// The getTheme() is returning a string
// which is the user's selected theme
editor.setTheme(getTheme());
Once we have an ACE editor initialized, the next part is to replace the onchage
and value setting with the ACE api's.
Let's see what happens when we change the content in the editor:
// Get the reference to the editor id
var currentEditorValue = editorValues.child(editorId);
// Get the `queue` child (which looks like an array where we push update events)
var queueRef = currentEditorValue.child("queue");
// This boolean is going to be true only when the value is being set programmatically
// We don't want to end with an infinite cycle since ACE editor triggers the
// `change` event on programmatic changes (which, in fact, is a good thing)
var applyingDeltas = false;
// Listen for the `change` event
editor.on("change", function(e) {
// In case the change is emitted by us, don't do anything
// (see below, this boolean becomes `true` when we receive data from Firebase)
if (applyingDeltas) {
return;
}
// Set the content in the editor object
// This is being used for new users, not for already-joined users.
currentEditorValue.update({
content: editor.getValue()
});
// Generate an id for the event in this format:
// <timestamp>:<random>
// We use a random thingy just in case somebody is saving something EXACTLY
// in the same moment
queueRef.child(Date.now().toString() + ":" + Math.random().toString().slice(2)).set({
// Store the data we get from ACE editor
event: e,
// Store the pseudo-user id
by: uid
}).catch(function(e) {
// In case of errors, we want to see them in the console
console.error(e)
});
});
Now that we know "saving stuff" in the database works, we can start waiting for updates:
// Listen for updates in the queue
queueRef.on("child_added", function (ref) {
// Get the timestamp
var timestamp = ref.key.split(":")[0];
// Do not apply changes from the past
if (openPageTimestamp > timestamp) {
return;
}
// Get the snapshot value
var value = ref.val();
// In case it's me who changed the value, I am
// not interested to see twice what I'm writing.
// So, if the update is made by me, it doesn't
// make sense to apply the update
if (value.by === uid) { return; }
// We're going to apply the changes by somebody else in our editor
// 1. We turn applyingDeltas on
applyingDeltas = true;
// 2. Update the editor value with the event data
doc.applyDeltas([value.event]);
// 3. Turn off the applyingDeltas
applyingDeltas = false;
});
Note that the change
event contains internal metadata about what was actually changed. It's working great with the applyDeltas
, which gets an array of event objects and simply applies these changes into another editor without user interaction. Since this is post specifically addresses ACE, we won't go too deep into explaining what the magic behind the applyDetlas
method is, but we can easily guess it has a lot of math going on.
TL;DR
The following image provides a better explanation for what is happening:
Setting the editor language
In a similar but much simpler way, we set the language selection:
// Select the desired programming language you want to code in
var $selectLang = $("#select-lang").change(function () {
// Set the language in the Firebase object
// This is a preference per editor
currentEditorValue.update({
lang: this.value
});
// Set the editor language
editor.getSession().setMode("ace/mode/" + this.value);
});
...
// Somebody changed the lang. Hey, we have to update it in our editor too!
currentEditorValue.child("lang").on("value", function (r) {
var value = r.val();
// Set the language
var cLang = $selectLang.val();
if (cLang !== value) {
$selectLang.val(value).change();
}
});
Setting the theme
Since we don't want the theme to be propagated to other users (e.g. personally, I love dark themes, but maybe someone else likes lighter themes), we will store the selected theme in the local storage:
// This function will return the user theme or the Monokai theme (which
// is the default)
function getTheme() {
return localStorage.getItem(LS_THEME_KEY) || "ace/theme/monokai";
}
// Select the desired theme of the editor
$("#select-theme").change(function () {
// Set the theme in the editor
editor.setTheme(this.value);
// Update the theme in the localStorage
// We wrap this operation in a try-catch because some browsers don't
// support localStorage (e.g. Safari in private mode)
try {not focus
localStorage.setItem(LS_THEME_KEY, this.value);
} catch (e) {}
}).val(getTheme());
We won't paste the whole file here in this tutorial, but
feel free to check it out on GitHub.
But how can I host it on the Internet?
Since the code is hosted on GitHub, GitHub Pages
is probably the easiest and fastest way to go live.
- You have to go in to "Settings" of your GitHub repository (
https://github.com/<owner>/<repo-name>/settings
) - Scroll down to the "GitHub Pages" section
- Select the
master
branch (we don't have fancy stuff in multiple branches) - Click
Save
and the project will go alive athttps://<owner>.github.io/<repo-name>
.
In our case, Emma is the owner, and the repository name is FireEdit
. You can access the application at:
Ideas
We have some ideas you may want to implement. Here are few of them:
- π Authentication
- β‘οΈ A cli tool connecting to the same editor and allowing you to edit
the content right in your terminal. - β° Revision history
- βοΈ Improve the security rules (this is strongly related to authentication)
- π₯ Show active users
One more thing
Feel free to open an issue
for any feature/bug/improvement. Contributions are welcome too!
Enjoy editing stuff with FireEdit and don't do it alone. π
If you build something similar or based on FireEdit, don't forget to ping us:
@EmanuelaColta and @IonicaBizau. π
PS: Yes, we edited this article using FireEdit. π
Iβve been trying to run this but I canβt figure out the part which deal with firebase data. How and where do I define the database schema on firebase? After running the application, it keeps on saying βeditorβ not found.
Release a source code without using ACE editor .I really didnβt catch up what you did in the last step :β(
love this!