In this tutorial we will build a very basic to-do web app. You can think of this tutorial as a demonstration of the core functionality of Userbase in the simplest way possible. We are going to focus solely on building a functional web app. Making things pretty is left as an exercise to the reader.
The entire web app we'll be building will fit in a single static HTML file of 182 lines. You can also see a live demo of the final result.
Let's get going. Open a new file in your favorite code editor.
code ugly-todo.html
And add some boilerplate HTML.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Ugliest To-Do</title>
</head>
<body>
<!-- application code -->
<script type="text/javascript">
</script>
</body>
</html>
Now, open this file in a web browser of your choosing. At this point all you'll see is a blank page. As we add functionality throughout the tutorial, you can refresh this page to see the changes.
To complete this tutorial, you'll need to create a Userbase admin account. Upon creation, a default application named "Trial" will be created. Take note of the App ID because we'll need it very soon.
We're going to load the Userbase SDK from a CDN with a <script>
tag in the head of our page.
The Userbase SDK will now be accessible via the userbase
variable. This will be our only dependency.
Before doing anything with the Userbase SDK, we need to let it know our App ID. Simply replace 'YOUR_APP_ID'
with the App ID you received when you created your admin account.
<body>
<!-- application code -->
<script type="text/javascript">
userbase.init({ appId: 'YOUR_APP_ID' })
</script>
</body>
</html>
Before our users can start creating to-dos, we need to give them a way to create an account with our app.
First, let's add a sign up form.
<body>
<!-- Auth View -->
<div id="auth-view">
<h1>Create an account</h1>
<form id="signup-form">
<input id="signup-username" type="text" required placeholder="Username">
<input id="signup-password" type="password" required placeholder="Password">
<input type="submit" value="Create an account">
</form>
<div id="signup-error"></div>
</div>
<!-- application code -->
<script type="text/javascript"></script>
</body>
Then, let's add the code to handle the form submission.
<!-- application code -->
<script type="text/javascript">
userbase.init({ appId: 'YOUR_APP_ID' })
function handleSignUp(e) {
e.preventDefault()
const username = document.getElementById('signup-username').value
const password = document.getElementById('signup-password').value
userbase.signUp({ username, password })
.then((user) => alert('You signed up!'))
.catch((e) => document.getElementById('signup-error').innerHTML = e)
}
document.getElementById('signup-form').addEventListener('submit', handleSignUp)
</script>
Now, whenever someone submits the form, the handleSignUp
function will be called. This gets the values of the username and password inputs and calls userbase.signUp({ username, password })
to create a new user account with Userbase.
Go ahead and reload the page in your browser. Enter a username and password in the form and submit. You should get an alert saying that you signed up. And if you go to your Userbase admin account, you should also see the new user under your app.
Now try signing up for another account using the same username and you'll see an error message displayed under the form.
We'll come back to this function in a bit to make it do something more interesting.
Now that our users can create accounts, let's give them the ability to login.
First, let's add a "Login" form to the page above our "Create Account" form.
<body>
<!-- Auth View -->
<div id="auth-view">
<h1>Login</h1>
<form id="login-form">
<input id="login-username" type="text" required placeholder="Username">
<input id="login-password" type="password" required placeholder="Password">
<input type="submit" value="Sign in">
</form>
<div id="login-error"></div>
<h1>Create an account</h1>
<form id="signup-form">
Then, let's add the code to handle the form submission.
<!-- application code -->
<script type="text/javascript">
userbase.init({ appId: 'YOUR_APP_ID' })
function handleLogin(e) {
e.preventDefault()
const username = document.getElementById('login-username').value
const password = document.getElementById('login-password').value
userbase.signIn({ username, password })
.then((user) => alert('You signed in!'))
.catch((e) => document.getElementById('login-error').innerHTML = e)
}
function handleSignUp(e) {
e.preventDefault()
…
</script>
And finally, let's bind our login form with our login handler.
<!-- application code -->
<script type="text/javascript">
…
.catch((e) => document.getElementById('signup-error').innerHTML = e)
}
document.getElementById('login-form').addEventListener('submit', handleLogin)
document.getElementById('signup-form').addEventListener('submit', handleSignUp)
</script>
</body>
You'll notice that this looks very similar to the sign up code from before. The handleLogin
function gets the values of the username and password inputs, and calls userbase.signIn({ username, password })
. This will attempt to sign in the user, handling a success with an alert and a failure by displaying the error.
Reload the page and you should see our new login form. Enter the username and password you used to create an account in the previous step, and submit the form. You should get an alert saying that you signed in.
Try submitting the form again with incorrect credentials and you'll see an error message displayed under the form.
After a user signs in, we'll want to hide the authentication forms and display their to-do list. First, let's add a container for the to-do list under the authentication forms.
<!-- Auth View -->
<div id="auth-view">
…
</div>
<!-- To-dos View -->
<div id="todo-view">
<div id="username"></div>
<h1>To-Do List</h1>
<div id="todos"></div>
</div>
<!-- application code -->
<script type="text/javascript">
userbase.init({ appId: 'YOUR_APP_ID' })
…
</script>
Then, let's make this view hidden by default, and add a function to display it.
<!-- application code -->
<script type="text/javascript">
…
.catch((e) => document.getElementById('signup-error').innerHTML = e)
}
function showTodos(username) {
document.getElementById('auth-view').style.display = 'none'
document.getElementById('todo-view').style.display = 'block'
document.getElementById('username').innerHTML = username
}
document.getElementById('login-form').addEventListener('submit', handleLogin)
document.getElementById('signup-form').addEventListener('submit', handleSignUp)
document.getElementById('todo-view').style.display = 'none'
</script>
Now that we have a function to show a view for signed in users, let's change
handleLogin
to call this function instead of showing an alert.
function handleLogin(e) {
e.preventDefault()
const username = document.getElementById('login-username').value
const password = document.getElementById('login-password').value
userbase.signIn({ username, password })
.then((user) => showTodos(user.username))
.catch((e) => document.getElementById('login-error').innerHTML = e)
}
And we do the same thing for handleSignUp
.
function handleSignUp(e) {
e.preventDefault()
const password = document.getElementById('signup-password').value
userbase.signUp({ username, password })
.then((user) => showTodos(user.username))
.catch((e) => document.getElementById('signup-error').innerHTML = e)
}
Reload the page and login again using your username and password. You should see the authentication view disappear and your username show up along with the to-do list heading.
Every time a user signs in, we need to establish a connection with the database that will hold the user's to-dos.
First, let's add a couple of elements for showing a loading indicator and error messages.
Then, let's change showTodos
to open a new database connection.
function showTodos(username) {
document.getElementById('auth-view').style.display = 'none'
document.getElementById('todo-view').style.display = 'block'
// reset the todos view
document.getElementById('username').innerHTML = username
document.getElementById('todos').innerText = ''
document.getElementById('db-loading').style.display = 'block'
document.getElementById('db-error').innerText = ''
userbase.openDatabase({ databaseName: 'todos', changeHandler })
.catch((e) => document.getElementById('db-error').innerText = e)
}
function changeHandler(items) {
document.getElementById('db-loading').style.display = 'none'
const todosList = document.getElementById('todos')
if (items.length === 0) {
todosList.innerText = 'Empty'
} else {
// render to-dos, not yet implemented
}
}
document.getElementById('login-form').addEventListener('submit', handleLogin)
document.getElementById('signup-form').addEventListener('submit', handleSignUp)
We changed showTodos
to make a call to userbase.openDatabase({ databaseName: 'todos', changeHandler })
. After the 'todos'
database is opened, our callback
function changeHandler
will be called whenever data changes in the database. When the Promise is resolved, the database is ready for use and we hide the loading indicator.
Reload the page and sign in again. You'll see "Loading to-dos..." while the connection to the database is getting established, followed by "Empty", indicating there are currently no to-dos in the database.
If the database has items in it, we'll want to render those under our to-do list.
Let's implement that in changeHandler
.
Now, let's add a form for creating new to-dos.
Then, let's add the code to handle the form submission.
<!-- application code -->
<script type="text/javascript">
…
function addTodoHandler(e) {
e.preventDefault()
const todo = document.getElementById('add-todo').value
userbase.insertItem({ databaseName: 'todos', item: { 'todo': todo }})
.then(() => document.getElementById('add-todo').value = '')
.catch((e) => document.getElementById('add-todo-error').innerHTML = e)
}
document.getElementById('login-form').addEventListener('submit', handleLogin)
document.getElementById('signup-form').addEventListener('submit', handleSignUp)
document.getElementById('add-todo-form').addEventListener('submit', addTodoHandler)
document.getElementById('todo-view').style.display = 'none'
</script>
In addTodoHandler
we get the to-do text from the input, and then call userbase.insertItem
with the database name and the object we want the persist. This
will return a Promise that resolves when the data is successfully persisted to
the database.
Reload the page and add some to-dos. Then, reload the page again and the to-dos should automatically appear after you login. These to-dos have been successfully persisted in the end-to-end encrypted Userbase database.
Now, let's add a checkbox to allow to-dos to be marked as completed.
// render all the to-do items
for (let i = 0; i < items.length; i++) {
// build the todo checkbox
const todoBox = document.createElement('input')
todoBox.type = 'checkbox'
todoBox.id = items[i].itemId
todoBox.checked = items[i].item.complete ? true : false
todoBox.onclick = (e) => {
e.preventDefault()
userbase.updateItem({ databaseName: 'todos', itemId: items[i].itemId, item: {
'todo': items[i].item.todo,
'complete': !items[i].item.complete
}})
.catch((e) => document.getElementById('add-todo-error').innerHTML = e)
}
// build the todo label
const todoLabel = document.createElement('label')
todoLabel.innerHTML = items[i].item.todo
…
// append the todo item to the list
const todoItem = document.createElement('div')
todoItem.appendChild(todoBox)
todoItem.appendChild(todoLabel)
todosList.appendChild(todoItem)
}
Reload the page and complete some to-dos. Their state should persist even after you reload the page and login again.
And finally, let's create a button for deleting a to-do.
// render all the to-do items
for (let i = 0; i < items.length; i++) {
// build the todo delete button
const todoDelete = document.createElement('button')
todoDelete.innerHTML = 'X'
todoDelete.style.display = 'inline-block'
todoDelete.onclick = () => {
userbase.deleteItem({ databaseName: 'todos', itemId: items[i].itemId })
.catch((e) => document.getElementById('add-todo-error').innerHTML = e)
}
// build the todo checkbox
const todoBox = document.createElement('input')
todoBox.type = 'checkbox'
And now let's append the delete button to the to-do element.
// append the todo item to the list
const todoItem = document.createElement('div')
todoItem.appendChild(todoDelete)
todoItem.appendChild(todoBox)
todoItem.appendChild(todoLabel)
todosList.appendChild(todoItem)
Reload the page and delete some to-dos. They should no longer show up even after you reload the page and login again.
Before we wrap up, let's add two final pieces of account functionality: user logout and automatic login for returning users.
First, let's add a logout button along with a container for error messages.
Then, let's add the code to handle the logout.
<!-- application code -->
<script type="text/javascript">
…
.catch((e) => document.getElementById('signup-error').innerHTML = e)
}
function handleLogout() {
userbase.signOut()
.then(() => showAuth())
.catch((e) => document.getElementById('logout-error').innerText = e)
}
function showTodos(username) {
document.getElementById('auth-view').style.display = 'none'
document.getElementById('todo-view').style.display = 'block'
…
function showAuth() {
document.getElementById('todo-view').style.display = 'none'
document.getElementById('auth-view').style.display = 'block'
document.getElementById('login-username').value = ''
document.getElementById('login-password').value = ''
document.getElementById('login-error').innerText = ''
document.getElementById('signup-username').value = ''
document.getElementById('signup-password').value = ''
document.getElementById('signup-error').innerText = ''
}
function changeHandler(items) {
const todosList = document.getElementById('todos')
…
document.getElementById('login-form').addEventListener('submit', handleLogin)
document.getElementById('signup-form').addEventListener('submit', handleSignUp)
document.getElementById('add-todo-form').addEventListener('submit', addTodoHandler)
document.getElementById('logout-button').addEventListener('click', handleLogout)
document.getElementById('todo-view').style.display = 'none'
</script>
The handleLogout
function calls userbase.signOut
which sends a request to end the user's session. A Promise is returned that resolves when the user is signed out, in which case we hide the to-do view and show the authentication view.
Let's modify our app to automatically sign in a returning user when the page loads. First, we'll add a loading indicator that will show while the app is trying to automatically sign in the user.
</head>
<body>
<!-- Loading View -->
<div id="loading-view">Loading...</div>
<!-- Auth View -->
<div id="auth-view">
<h1>Login</h1>
Then, let's add the following to our userbase.init
call.
<!-- application code -->
<script type="text/javascript">
userbase.init({ appId: 'YOUR_APP_ID' })
.then((session) => session.user ? showTodos(session.user.username) : showAuth())
.catch(() => showAuth())
.finally(() => document.getElementById('loading-view').style.display = 'none')
…
document.getElementById('login-form').addEventListener('submit', handleLogin)
document.getElementById('signup-form').addEventListener('submit', handleSignUp)
document.getElementById('add-todo-form').addEventListener('submit', addTodoHandler)
document.getElementById('logout-button').addEventListener('click', handleLogout)
document.getElementById('todo-view').style.display = 'none'
document.getElementById('auth-view').style.display = 'none'
</script>
We are now hiding the authentication view by default so that it will only show if an existing session can't be resumed.
The userbase.init
function returns a Promise that resolves when the SDK has determined if it can reuse the previous session. If so, the user gets automatically logged in, and the session.user
object gets set. If there was no previous session, or the session cannot be resumed, the session.user
object will not be set. If userbase.init
fails, we'll just send the user
to the sign in page regardless of the reason.
Now, if we sign in and reload the page, we will get signed in automatically without having to re-enter our username and password. However, if we want the session to persist even after we close the browser's window, we need to set the rememberMe
parameter to 'local'
when calling userbase.signIn
and userbase.signUp
.
<!-- application code -->
<script type="text/javascript">
userbase.init({ appId: 'YOUR_APP_ID' })
function handleLogin(e) {
e.preventDefault()
const username = document.getElementById('login-username').value
const password = document.getElementById('login-password').value
userbase.signIn({ username, password, rememberMe: 'local' })
.then((user) => showTodos(user.username))
.catch((e) => document.getElementById('login-error').innerHTML = e)
}
function handleSignUp(e) {
e.preventDefault()
const username = document.getElementById('signup-username').value
const password = document.getElementById('signup-password').value
userbase.signUp({ username, password, rememberMe: 'local' })
.then((user) => showTodos(user.username))
.catch((e) => document.getElementById('signup-error').innerHTML = e)
}
…
</script>
And that was it! A fully working (but ugly) web app in just 182 lines of code, including markup and comments. If you have any questions, or there's anything we can do to help you with your web app, please get in touch. Thank you!