"Hello World" App
First, Some Boilerplate
Before we get to the Edsu-specific parts, here's three utility functions we use that are good to know about:
$(x)
is an alias todocument.querySelector(x)
log(x)
puts text in the box below the colour pickerlogCatch(x)
catches async errors and logs them
With that out of the way, let's get to the app!
The App
document.addEventListener('DOMContentLoaded', logCatch(async () => {
// Config
let name = 'prv.app.edsu-org.hello-world.storage';
let key = 'color';
// Connect to their Edsu account
let storage = await storageConnect(name);
// Load the existing colour from storage
log('Loading colour...');
$('#picker').value = await storage.getJson(key) || '#ffffff';
log('Colour loaded');
// If the user changes the colour, save it to their storage
$('#picker').addEventListener('change', logCatch(async () => {
await storage.clobberJson(key, $('#picker').value);
log('Colour saved');
}));
// Track colour changes
storage.subPut(key, {}, {
notify: logCatch(async () => {
$('#picker').value = await storage.getJson(key) || '#ffffff';
log('Colour loaded');
}),
});
}));
This is the core functionality of the app. It starts with a function call, storageConnect()
, which we'll get to in a minute, followed by 3 sections of functionality.
Loading From Storage
The HTML for this app pulls in three external Javascript files:
The first, edsu-raw.js, is a thin wrapper over the Edsu protocol - it puts and gets blocks and names, etc. However, it's quite low level, so usually you'll see it accompanied by another library which wraps it with more purpose-specific functionality - this is what edsu-storage-basic.js is.
As we'll see, storageConnect()
returns a StorageBasic
instance (defined in edsu-storage-basic.js). This instance has a small number of methods, useful if all you want to do is store some application state. It keeps data in a single name, indexed by a key
.
So, the call to storage.getJson(key)
returns whatever we last stored at the name 'prv.app.edsu-org.hello-world.storage' and the key 'color'. The "Json" part of the function name means that it conveniently decodes the JSON it finds before returning. If nothing's been stored yet, it returns a null
.
So, this is all the code we need to pull JSON stored in Edsu and set the colour picker to what is stored there (or a default if not found):
$('#picker').value = await storage.getJson('color') || '#ffffff';
Storing Back Again
We then set up a callback for when the user changes the colour. This essentially reverses the previous section: we grab the colour from the picker and then use clobberJson()
to store it back at that name and key.
Here's the line that does all the work:
await storage.clobberJson('color', $('#picker').value);
Watching For Changes
It's also easy to ask to be notified if the colour changes (for instance, if you open up another browser window and go to the same app). A sub is essentially an Edsu subscription that's filtered to the key
that you're interested in. In the above code we just duplicated the code we used to load the colour and put it in the notify callback.
And that's it for the app! Hopefully it's easy to see how you could extend this to keep not just colours but any kind of application state - preferences, drafts of documents or messages, etc.
Now let's get into the one detail we left for later - the storageConnect()
call.
Setting Up a Connection
async function storageConnect(name) {
if (!localStorage.getItem('token'))
await tokenGet(name);
let username = localStorage.getItem('username');
let token = localStorage.getItem('token');
return new StorageBasic(username, token, name, {}, { err: log });
}
This part is easy - we just need the name, username, and permissions token to establish the StorageBasic-wrapped Edsu connection.
The name we've already seen passed to this function. The fourth - { err: log }
- argument is just asking that any connection errors be logged.
The trouble is that we don't have username
and token
. These are core concepts in Edsu: instead of "logging in", users grant each individual app permissions to use parts of their Edsu account's storage, with those permissions embodied in tokens.
This is what the tokenGet()
function is for - if the user hasn't done so already, they need to grant this app the permission to use their storage to keep their colour choice.
Getting Permission
async function tokenGet(name) {
// Create the widget for the username, and prompt the user
let username = await new Promise(resolve => {
let widget = edsuUsernameElement(resolve);
$('#username').appendChild(widget);
log('Please enter your Edsu username');
});
// We have the username - delete the widget
$('#username').innerText = '';
// Now make the grant request
let label = 'The "Hello World" App';
let explanation = 'Just storing some colour information!';
let grantReq = StorageBasic.generateGrantRequest(name, label, explanation);
let grant = await edsuAsyncGrantRequest(username, grantReq);
// Save the token and the username in localStorage
localStorage.setItem('token', grant.tokenOwner);
localStorage.setItem('username', username);
}
Edsu is a federated protocol - everything begins with a username, which could point to a host anywhere on the internet. So we start by asking the user for theirs.
Getting the Username
The external javascript file used by this app that we haven't talked about yet is edsu-grant.js, which is code to help us ask for the permissions we need for our app. It has a function, edsuUsernameElement()
, which takes a callback and returns an HTML element.
The element is a widget styled in a way as to be instantly recognizable to Edsu users - this helps with UX - where they can enter their username. Once they do that, the callback you provided is called with whatever the user entered as its sole argument.
Making the Grant Request
The other function we use from edsu-grant.js is edsuAsyncGrantRequest()
, and it has two parameters: username
and request
. We've gotten the username already, we just need the request.
Conveniently, StorageBasic has a static method generateGrantRequest()
which generates a grant request object sufficient to fill its permission needs (i.e. read and write the name we're using). Besides the name, it takes two arguments: a label for the grant, and an explanation.
The label is for later when the user is looking at all the apps they've granted permissions to, so they can remember what this one was for. It's just a suggestion - the user can label this grant whatever they like.
The explanation is for during the grant process. The user has to decide whether the permissions requested are reasonable - an explanation helps them make that decision, e.g. if it's not obvious why you need access to a certain name, this is the place to mention why.
With these two arguments, we can make the call to generateGrantRequest()
, store the token we get back from it, and then continue from where we left off: establishing a connection, and then setting up the app.
We're done!
From Here
The three larger functions in this app represent three activities that will be present in nearly every Edsu app: getting permissions, connecting to Edsu servers using those permissions, and storing/retrieving data from those servers.
Now that you've seen how it works in a toy app, you've got all you need to write your own :) All you need is static hosting - there's no server of your own to set up.
When you'd like to get more details on Edsu, its capabilities, and how to use them, the in-depth section is where you'll find library documentation, the specification of the Edsu protocol, and other resources.
Thanks for reading!