Tutorial: Build a Zero Dependency Notes App on IPFS — Part II

Justin Hunter
SimpleID — Engagement and Retention
12 min readSep 3, 2019

In the first part of this tutorial, we set up authentication for our application. We did so with no dependencies and just two files — index.html and main.js. In this second part of the tutorial, we’ll build out the application, which is a simple note-taking app with content stored on IPFS. Since we are using SimpleID’s APIs, the IPFS content storage happens through the fantastic service provided by Pinata.

I think the best place to start is in the html. We already have a section ready to hold our application content. Find the section that looks like this:

<div style="display: none" id="root-app">
<button onclick="signOut()">Sign Out</button>
<h1>This is the app</h1>
</div>

The sign out button can stay, but we need to remove the This is the app text and replace it with what our actual application content will be. A couple of things, I’m thinking about already are:

  • How do we switch between note-taking and viewing all existing notes?
  • How will all existing notes be rendered?

Let’s set up our html content to support what will hopefully be decent answers to those questions:

<div style="display: none" id="root-app">
<button onclick="signOut()">Sign Out</button>
<div id="notes-collection">
<ul id="notes-collection-items"></ul>
</div>
<div style="display: none;" id="single-note">
</div>
</div>

When we display the application content, we don’t want to display both the note-taking and the notes collection at once. We could, but I think it’d be cleaner to show the user’s collection of notes, and then on a button push or something, the note taking section will be rendered. So, we start with the notes-collection rendered and the single-note hidden.

You’ll note that within the notes collection, I’ve included a ul without any children li. That’s because we’re going to programmatically render those list-items as our notes collection is loaded.

Now that we have the skeleton of our application content, let’s jump back into our JavaScript file and start by fetching our notes-collection index file. This file will contain an array of identifiers, titles, and dates for all our notes. In main.js, we first need to add a new global variable at the top of the file. Below the loading variable add let notesCollection = [];. Now, add a new function called fetchCollection():

async function fetchCollection() {  const url = "https://api.simpleid.xyz/fetchContent";  const username = JSON.parse(localStorage.getItem('user-session')).username;
const notesId = "notesIndex";
const data = `username=${username}&devId=${config.devId}&devSuppliedIdentifier=${notesId}&development=true`;
const pinnedContent = await postToApi(data, url); if(!pinnedContent.includes("ERROR")) {
notesCollection = JSON.parse(pinnedContent);
} else {
notesCollection = [];
}
}

If you remember, we were smart and created a reusable function to posting to the SimpleID API that always returns a promise. Here’s we’re just specifying the url to post to and the data to include. The data we are sending has to include an identifier that will be used for finding the right file from the IPFS network. That’s the notesId. You might be asking how that identifier can be used with multiple users while still returning the correct content for each user. That’s where the username variable comes in. In the data we are posting, the username should be your logged in user’s username. Remember, this is stored in localStorage, so we can easily fetch that and include it.

When we make our request to the API, we need to account for any error that might arise, including a lack of content being stored previously. So, we check for the error, and if there is none, we set our notesCollection array with the response. Otherwise, we set the notesCollection to an empty array.

But this isn’t quite enough. We need to loop through that array and add a list-item for each note between our notes-collection-items ul. We can do that if there is no error. So in that if block add this below notesCollection = JSON.parse(pinnedContent) let’s add a call to a function called renderCollection(). Then we can create the renderCollection function like this:

function renderCollection() {
let list = document.getElementById('notes-collection-items');
list.innerHTML = "";
for(const note of notesCollection) {
let item = document.createElement('li');
item.appendChild(document.createTextNode(note.title));
item.appendChild(document.createTextNode(note.date));
item.setAttribute("id", note.id);
list.appendChild(item);
}
}

Here’s we’re just grabbing the ul we created, created children list-items beneath it and filling those list items with the note title and note date. We are also setting the note’s id as the element id. This will come in handy later when we want to display the actual content of the note.

This needs to be called once the user logs in and once the user signs up, so let’s wire that up:

//in signin function...
if(!userData.includes('ERROR')) {
console.log(userData);
let userSession = JSON.parse(userData);
userSession.username = username;
localStorage.setItem('user-session', JSON.stringify(userSession));
loading = false;
loggedIn = true;
pageLoad();
fetchCollection(); //call it here for sign in
} else {
loading = false;
loggedIn = false;
pageLoad();
console.log("Error");
}
//in sign up function...
const keyData = `username=${username}&password=${password}&profile=${uriEncodedProfile}&url=https%3A%2F%2Fthisisnew.com&development=true&devId=imanewdeveloper`;
const userData = await postToApi(keyData, urlAppKeys);
if(!userData.includes('ERROR')) {
console.log(userData);
let userSession = JSON.parse(userData);
userSession.username = username;
localStorage.setItem('user-session', JSON.stringify(userSession));
loading = false;
loggedIn = true;
pageLoad();
fetchCollection(); //call it here
} else {
loading = false;
loggedIn = false;
pageLoad();
console.log("Error");
}

We also need to call fetchCollection on every page load because the user might refresh the screen. We should only do that if the user is logged in though. So right below the pageLoad() call toward the top of main.js add this:

pageLoad();if(localStorage.getItem('user-session')) {
fetchCollection();
}

Now, we already know there’s no content to display yet, so let’s think about how we want to enable new notes to be created. I mentioned creating a button before, and I kind of like that idea. Let’s add a few things to our application content:

<div style="display: none" id="root-app">
<button onclick="signOut()">Sign Out</button>
<div id="notes-collection">
<div>
<h3 id="notes-total"></h3>
<button onclick="newNote()" id="create-note">New Note</button>
</div>
<ul id="notes-collection-items"></ul>
</div>
<div style="display: none;" id="single-note">
<button onclick="closeNote()">Close</button>
<ul id="toolbar">
<li>Bold</li>
<li>Italics</li>
<li>Underline</li>
<li class="right">Save</li>
</ul>
<div>
<input type="text" id="title" placeholder="note title" />
</div>
<div id="note" contenteditable="true"></div>
</div>
</div>

We’ve done a few things here. We’ve added a button to let us create new notes and we’ve binded an event handler to it that calls newNote(). We’ll set up that function in a minute. We added a button to close the note screen once it’s open. It calls a function that will…well…close the note screen. We also added a toolbar to our single note section. This will be a simple note-taking app with simple functionality — bold, italics, underline. We also have a save button in our toolbar that we will wire up later. We need to give our notes a title, so we also added a title input field. Finally, we added a contenteditable div to hold our note and gave it an id we can reference later.

What now? Well, I think we should make the New Note button show our new note, right? Let’s do that in our main.js file. Open that up and add a function called newNote():

function newNote() {
document.getElementById('notes-collection').style.display = "none";
document.getElementById('single-note').style.display = "block";
document.getElementById('note-title').value = "";
document.getElementById('note').innerHTML = "";
}

In the newNote() function we are hiding the notes collection because we don’t need to see that while writing a new note. We are then showing the note taking screen. We are also setting the note title and note content to an empty string because every time New Note is pressed, it should be a fresh note with no pre-filled content.

While we’re here, let’s set up that closeNote() function as well:

function closeNote() {
document.getElementById('notes-collection').style.display = "block";
document.getElementById('single-note').style.display = "none";
}

This one is pretty simple. We’re hiding the note taking screen and showing the note collection screen again. Something to note (see what I did there?) here is that notes are only saved when the user clicks Save note. We haven’t set that up yet, but we will. This means, when the user clicks the close button, nothing is being saved. You’re welcome to wire this up differently.

I think the next two things we should do are handle changes to the content in our note taking section (when the user types, we want to track that) and handle the toolbar buttons. Let’s start with tracking content changes. For that, we’re going to add an event listener to our main.js file. You can do this above the first function but below the pageLoad() call like this:

let editable = document.getElementById('note');
editable.addEventListener('input', function() {
console.log('You are typing');
});

Save that, then when signed into your app, go ahead and create a new note and type something. Open up the developer console, and you should see You are typing printed out. That’s cool! But it’s less than useful. We need to track the actual content so that when it’s time to save, we have a variable to use. To do that, let’s create a new global variable at the top of your main.js file right below the notesCollection variable:

let noteContent = "";

Now, within the event listener, we can remove the console.log and replace it with the innerHTML of the contenteditable div that represents our note and set equal to the noteContent variable. It should look like this:

let editable = document.getElementById('note');
editable.addEventListener('input', function() {
noteContent = editable.innerHTML;
console.log(noteContent);
});

I added a console.log below that to make sure all is working. Do that if you want and test it by creating a new note, typing, and opening the developer console. If you do that, you should see the html representation of what you type out printed in the console.

Now, let’s make this text formatted! For our toolbar, we’ll need to add an onclick event handler for each item. When each toolbar button is clicked, it should apply the formatting specified. Thankfully, there’s some built-in JavaScript to support this. Let’s give it a shot:

<ul id="toolbar">
<li onmousedown="event.preventDefault()" onclick="document.execCommand('bold', false, null)">Bold</li>
<li onmousedown="event.preventDefault()" onclick="document.execCommand('italic', false, null)">Italics</li>
<li onmousedown="event.preventDefault()" onclick="document.execCommand('underline', false, null)">Underline</li>
<li class="right">Save Note</li>
</ul>

We’re using the built in execCommand functionality in JavaScript to build a very simple WYSIWYG editor for our note-taking app. Pretty cool, huh? You’ll notice the onmousedown event handler. This is there because when you click the toolbar buttons, focus is taken away from the text that needs to be formatted, thus making it seem like the buttons don’t work. We prevent that with this event handler.

Ok, this is all working. Now, we need to wire up our Save Note button. Let’s think about what this needs to do:

  • Create an object that holds the note title, the note content, and the note id (which we can generate pretty easily)
  • Add the new note to the existing notesCollection array
  • Save an index file that has all of the notesCollection array
  • Save the note file itself which includes the full content

Let’s start by adding an event handler to our Save Note button in the toolbar:

<ul id="toolbar">
<li onmousedown="event.preventDefault()" onclick="document.execCommand('bold', false, null)">Bold</li>
<li onmousedown="event.preventDefault()" onclick="document.execCommand('italic', false, null)">Italics</li>
<li onmousedown="event.preventDefault()" onclick="document.execCommand('underline', false, null)">Underline</li>
<li onclick="saveNote()" class="right">Save Note</li>
</ul>

Ok, now, let’s actually create that function in our main.js file:

async function saveNote() {
let note = {
id: Date.now(),
title: document.getElementById('note-title').value === "" ? "Untitled" : document.getElementById('note-title').value
}
console.log(note);
notesCollection.push(note);
console.log(notesCollection);
}

We’re taking baby steps to make sure things work, but go ahead and test this out. Start a new note, write something, give it a title, then save it. In console, you should see the individual note and you should see the notesCollection array was updated. You’ll notice our note object does not include the content. That’s because the first thing we’re doing is updating our index of notes, which only requires basic meta data.

The next thing we should do is save the index file to IPFS. That’s right, you’ve waited all this time. Let’s save some content!

To test this, let’s update our saveNote() function:

async function saveNote() {
let note = {
id: Date.now(),
title: document.getElementById('note-title').value === "" ? "Untitled" : document.getElementById('note-title').value
}
notesCollection.push(note);
const pinURL = "https://api.simpleid.xyz/pinContent"; const username = JSON.parse(localStorage.getItem('user-session')).username; const identifier = "notesIndex"; const content = encodeURIComponent(JSON.stringify(notesCollection)); const data = `username=${username}&devId=${config.devId}&devSuppliedIdentifier=${identifier}&contentToPin=${content}&development=true`; const postedContent = await postToApi(data, pinURL); if(!postedContent.includes("ERROR")) {
console.log(postedContent);
} else {
console.log("Error pinning content");
console.log(postedContent);
}
}

Here, we are specifying the API endpoint, we are grabbing the logged-in user’s username, we’re URIEncoding the content, and then we are building up a from-data-compliant data string. That is all being sent to the postToApi() function. We check the response from that API call and look for an error. If there’s no error, we’re ready to move on. If there is, we console log it.

Now, this is only half of what we need to do. We also need to save the individual note with its content. So let’s do that. Inside, the if(!postedContent.indludes("ERROR") block, let’s add this:

note.content = noteContent;
const noteIdentifier = JSON.stringify(note.id);
const saveNoteContent = encodeURIComponent(JSON.stringify(note));
const noteData = `username=${username}&devId=${config.devId}&devSuppliedIdentifier=${noteIdentifier}&contentToPin=${saveNoteContent}&development=true`;
const postedNote = await postToApi(noteData, pinURL);console.log(postedNote);if(!postedNote.includes("ERROR")) {
document.getElementById('notes-collection').style.display = "block";
document.getElementById('single-note').style.display = "none";
renderCollection();
} else {
console.log("Error posting note content");
console.log(postedNote);
}

We’re basically doing exactly what we did when saving the note collection index file, but we’re doing it for the individual note. See how we’re adding the noteContent variable to the note object? That’s because for the individual note, we want to load the full note, including content.

If all goes well, we are closing the note and showing the notes collection, but we’re also calling renderCollection to make sure we update the UI with the new note. It’s a good time to make a couple updates to our renderCollection function now, so let’s do that:

function renderCollection() {
let list = document.getElementById('notes-collection-items');
list.innerHTML = "";
for(const note of notesCollection) {
let item = document.createElement('li');
item.appendChild(document.createTextNode(note.title));
item.setAttribute("id", note.id);
item.onclick = () => loadNote(note.id);
item.style.cursor = "pointer";
list.appendChild(item);
}
}

We are adding a style attribute to our notes list-items so that when we hover over it, our mouse cursor turns to a pointer as if hovering over a link. We’re also adding an event listener that calls the loadNote function on click. This is pretty intuitive, I think, but we want to be able to click on the title of a note and load the full content of the note clicked.

Let’s stub out that loadNote function now:

async function loadNote(id) {
console.log(id);
}

Let’s test before moving on. If you save and refresh your browser, you should see (if you have saved any notes), the list of notes saved appear in the form of an unordered list. Click on the title of one of the notes and check your developer console. You should see the note’s id printed out.

We are just about done! All we need to do now is load up a note with its actual content so that the user can view or make edits as needed. Let’s finish things up by doing that in our loadNote() function:

async function loadNote(id) {
const url = "https://api.simpleid.xyz/fetchContent";
const username = JSON.parse(localStorage.getItem('user-session')).username;
const noteId = JSON.stringify(id);
const data = `username=${username}&devId=${config.devId}&devSuppliedIdentifier=${noteId}&development=true`;
document.getElementById('notes-collection').style.display = "none";
document.getElementById('single-note').style.display = "block";
const pinnedContent = await postToApi(data, url);
console.log(pinnedContent);
if(!pinnedContent.includes("ERROR")) {
noteContent = JSON.parse(pinnedContent).content;
document.getElementById('note-title').value = JSON.parse(pinnedContent).title;
document.getElementById('note').innerHTML = noteContent;
} else {
console.log("Couldn't load note")
}
}

Go ahead and test this now. Create a note, save it, open it. All seems to be working, but we forgot one thing. If we were to edit an existing note, it wouldn’t update the note in question, it would instead create a new note. Let’s fix that.

First, we need to make sure we set our individual note id to a global variable so that we can use it again when trying to save the updated note. So, at the top of your main.js file, add this global variable below noteContent = "":

let singleNoteId = null;

Then, in your loadNote function, add this at the beginning:

singleNoteId = id;

Now, go to your saveNote() function and update the top of that function with this:

let note = {
id: singleNoteId ? singleNoteId : Date.now(),
title: document.getElementById('note-title').value === "" ? "Untitled" : document.getElementById('note-title').value
}
let index = await notesCollection.map((x) => {return x.id }).indexOf(note.id);if(index < 0) {
//This is a new note
notesCollection.push(note);
} else if(index > -1) {
//The note exists and needs to be updated
notesCollection[index] = note;
} else {
console.log("Error with note index")
}

This should now let us create new notes AND update existing notes. Let’s give it a try and make sure. Open an existing note, edit it, then click the Save button.

You did it! You just built a zero dependency application that lets you do the following:

  • sign up
  • sign in
  • sign out
  • create notes
  • format notes
  • save to IPFS
  • fetch from IPFS

In addition to this being a zero dependency application, the entire size of the app, unbundled and unminified and unzipped is just under 20kb. That’s pretty great.

This whole app is super ugly, and the css I provide isn’t going to make it all that much better, but you can grab this CSS to make it at least look presentable. In fact, the entire project is loaded up here if you’d like to explore the repository or if you hit trouble along the way and need to review the code.

There’s a whole lot more you can do with this app, and your homework, if you choose to do it, is to try to add the following yourself:

  • More formatting options (beginner)
  • Delete notes (intermediate)
  • Password protected share link (advanced)

Something worth pointing out in this tutorial is that you are using an API key client-side. This is bad security practice. If you are concerned about others seeing your API key and using it, you should obscure this by standing up a server, making calls to that server, and using the SimpleID API functions on that server. You can protect your keys server-side. That’s well outside the scope of this tutorial though.

Thanks for sticking around for both parts of this tutorial. If you enjoyed this and want to use the simple authentication tools and simple IPFS tools, take a look at SimpleID. We’d love to see what you can build.

--

--