This article needs a technical review. How you can help.
The Alarm API is another tool at your disposal for providing a user with a system notification, using a more simplistic workflow for automatically setting notification deadlines and checking when they are due, rather than having to do all this manually via some kind of regular timer and complex checking function like we did in the previous article. This article shows how it's done.
The reference example
The example referred to throughout this article is To-do List Alarms, a to-do list example that stores to-do list information (task and time and date task is due) in an IndexedDB instance. When a to-do list task is stored, the Alarm API is used to store an Alarm reference in the system, which is handled by the same application. When the alarm is due, a system notification is shown to notify the user of this, which works whether the application is open at the time or not.
Browser support is as follows:
- The IndexedDB part of the application should work in Firefox desktop/android/OS, IE 10, Chrome and Opera 16+
- The alarm and notification part of the application is pretty experimental right now, and currently only works on apps installed on Firefox OS 1.2+ It is possible to get notifications working on other systems (as detailed in Notifying users...), but in this case the notifications rely on the alarms.
The To-do List Alarms code is available on Github, so grab it and play around with it. If you want to test it out on a real device, I'd recommend using the WebIDE.
This article won't focus much on IndexedDB: if you are interested in how that part of the app works and haven't got much experience with IndexedDB, check out our IndexedDB documentation, starting with Using IndexedDB.
With the example introduced, lets start working through the parts of the example relevant to setting our alarms.
Adding new data, and setting our alarms
One of the most important functions in the JavaScript for this file is addData()
: this grabs the data entered into the form, stores it in the IndexedDB, and then sets an alarm in the Firefox OS system. Let's go through it bit by bit.
function addData(e) { // prevent default - we don't want the form to submit in the conventional way e.preventDefault(); // Stop the form submitting if any values are left empty. This is just for browsers that don't support the HTML5 form // required attributes if(title.value == '' || hours.value == null || minutes.value == null || day.value == '' || month.value == '' || year.value == null) { note.innerHTML += '<li>Data not submitted — form incomplete.</li>'; return;
We begin by preventing standard form submission, and doing a check to make sure the form has been filled in: if there are empty fields, we don't submit and give the user a message to say that the form isn't complete (this is added into the messages display at the bottom left of the UI; run the app and check it out.)
Note: I have also included required
attributes on the form elements (look at the index.html
file to see them) to deal with the client-side validation automatically, HTML5 style! The above if()
block is only really for browsers that don't understand HTML5 forms.
} else { // test whether the Alarm API is supported - if so, we'll set a system alarm if(navigator.mozAlarms) { //build a date object out of the user-provided time and date information from the form submission var myAlarmDate = new Date(month.value + " " + day.value + ", " + year.value + " " + hours.value + ":" + minutes.value + ":00"); // The data object can contain any arbitrary data you want to pass to the alarm. Here I'm passing the name of the task var data = { task: title.value }
When the form is successfully submitted, we now turn our attention to our alarms! We first check to see if the browser supports the Alarm API using if(navigator.mozAlarms)
. If the mozAlarms
object exists, we then proceed to setting the alarm. We first need a valid Date()
object to state when the alarm should be set for; the long assignment value of myAlarmDate
above shows how we construct this out of the values inserted into the form elements. Last for this section of code, we store the task title in an arbitrary data object for use in setting the alarm below.
// The "ignoreTimezone" string makes the alarm ignore timezones and always go off at the same time wherever you are var request = navigator.mozAlarms.add(myAlarmDate, "ignoreTimezone", data); request.onsuccess = function () { console.log("Alarm sucessfully scheduled"); var alarmRequest = navigator.mozAlarms.getAll(); alarmRequest.onsuccess = function() { newAlarmId = this.result[(this.result.length)-1].id; } };
We now request
the navigator.mozAlarms.add()
method to set a new alarm in the Firefox OS system. This takes three arguments:
- A valid
Date()
object like the one we constructed earlier, to state when the alarm should be set for. - A keyword to state whether this alarm should ignore timezones (
ignoreTimezone
), and always go off relative to the time the current device is set at, or honor timezones (honorTimezone
), and go off at different times depending on what timezone the current device is set to (the system will detect the difference between the alarm's time zone, and the timezone of the current device, and adjust the time accordingly.) - An arbitrary data object (like the one we set above), which can contain whatever data you want to associate with the alarm being set.
Note: You need to use the same URL for setting and receiving an alarm. For example, If you invoke navigator.mozAlarms.add()
on foo.html or index.html?foo=bar, but have { "alarm": "/index.html" }
in your manifest messages field, you'll never receive the alarm.
Next, inside the onsuccess
handler of the request
we execute some code to grab the id
of the alarm that has just been set. To do this, we first use the getAll()
method of mozAlarms
to request an array containing objects representing all the alarms (alarmRequest
). We can grab the id of each one using the alarm's id
property, but how do we grab just the latest one? The answer is that the latest one is the last one in the array, so we grab it using this.result[(this.result.length)-1]
, then get its id and store it in a variable using newAlarmId = this.result[(this.result.length)-1].id
.
Note: The newAlarmId
variable was set at the top of the code, so it is global.
request.onerror = function () { console.log("An error occurred: " + this.error.name); }; } else { note.innerHTML += '<li>Alarm not created - your browser does not support the Alarm API.</li>'; };
The next section handles the failure cases: if the request
to set the alarm failed, we log its error message. If the Alarm API is not supported at all, we add a message to the application's message box to tell the user that alarms are not supported on their device.
// grab the values entered into the form fields and store them in an object ready for being inserted into the IDB var newItem = [ { taskTitle: title.value, hours: hours.value, minutes: minutes.value, day: day.value, month: month.value, year: year.value, notified: "no", alarmId: newAlarmId } ]; // open a read/write db transaction, ready for adding the data var transaction = db.transaction(["toDoListAlarms"], "readwrite"); // report on the success of opening the transaction transaction.oncomplete = function(event) { note.innerHTML += '<li>Transaction opened for task addition.</li>'; }; transaction.onerror = function(event) { note.innerHTML += '<li>Transaction not opened due to error. Duplicate items not allowed.</li>'; }; // call an object store that's already been added to the database var objectStore = transaction.objectStore("toDoListAlarms"); // add our newItem object to the object store var request = objectStore.add(newItem[0]); request.onsuccess = function(event) { // report the success of our new item going into the database note.innerHTML += '<li>New item added to database.</li>';
Moving on! This next code section handles adding the new to-do list item to the IndexedDB. I'll not explain this here, so for more help with IndexedDB, read Using IndexedDB.
// clear the form, ready for adding the next entry title.value = ''; hours.value = null; minutes.value = null; day.value = 01; month.value = 'January'; year.value = 2020; }; };
Now we clear the form, ready for the next to-do list item to be entered.
// update the display of data to show the newly added item, by running displayData() again. displayData(); };
Last of all, we run the displayData()
function, which updates the display of the to-do list items at the top of the app UI.
Responding to a set alarm
We have just set an alarm in the Firefox OS system, but that's all it is so far, just a system message. How do we respond to the alarm when it goes off? This can be handled from any application, using the currently Firefox OS-only navigator.mozSetMessageHandler()
method, and the code is really quite simple.
// This will handle an alarm set by this application if(navigator.mozSetMessageHandler) { navigator.mozSetMessageHandler("alarm", function (alarm) { // only launch a notification if the Alarm is of the right type for this app if(alarm.data.task) { // Create a notification when the alarm is due new Notification("Your task " + alarm.data.task + " is now due!"); updateNotified(alarm.data.task); } }); }
In this code we first use if(navigator.mozSetMessageHandler)
to check whether it is supported before running the rest of the code. If it is, then we use navigator.mozSetMessageHandler()
to run a function when the alarm happens. This takes two arguments:
- The type of message to respond to, in this case,
alarm
(basically, a system message is sent when an alarm goes off.) - A function to run when the alarm message is sent. Here we have an anonymous function that accepts the alarm as its argument.
Inside the function we could handle notifying the user that the alarm has happened in any way we like, but we will do it using a system notification, as this will show whether we have got the app open at the time or not. We first run an if(alarm.data.task)
check, to check whether the data.task
data item is attached to this alarm (remember the arbitrary data we attached to the alarms in the addData()
function we looked at above?) We do this because we don't want to fire a notification for every alarm in the system, just alarms that have been set by our application. Checking the alarm data is a convenient way to do this.
If this check passes, then we do two things:
- Create a system notification with
new Notification("Your task " + alarm.data.task + " is now due!");
— this will appear on the system as shown in the screenshot to the left. - Run the
updateNotified()
function, which changes thenotified
property of the current task's value fromNo
toYes
; the value of this property is checked for each task whendisplayData()
is called, and tasks that have already had a notification fired for them are given a different styling in the app UI.
Note: For more details on using the Notification API, read Notifying users via the Notification and Vibration APIs.
Removing an alarm
The last part of the code relevant to our current article is contained within the deleteItem()
function.
function deleteItem(event) { // retrieve the name and the alarm ID of the task we want to delete var dataTask = event.target.getAttribute('data-task'); var dataAlarm = event.target.getAttribute('data-alarmId'); //delete the alarm associated with this task if(navigator.mozAlarms) { navigator.mozAlarms.remove(dataAlarm); } // delete the parent of the button, which is the list item, so it no longer is displayed event.target.parentNode.parentNode.removeChild(event.target.parentNode); // open a database transaction and delete the task, finding it by the name we retrieved above var request = db.transaction(["toDoListAlarms"], "readwrite").objectStore("toDoListAlarms").delete(dataTask); // report that the data item has been deleted request.onsuccess = function(event) { note.innerHTML += '<li>Task \"' + dataTask + '\" deleted.</li>'; }; }
Each item listed in the UI using the displayData()
function has a delete <button>
; each one is given a data-alarmid
attribute containing the id of the alarm associated with this item, and a data-task
attribute containing the title of the task. When a delete button is pressed, the deleteItem()
function is run, which first grabs these id and title values out of those attributes and stores them in variables:
var dataTask = event.target.getAttribute('data-task'); var dataAlarm = event.target.getAttribute('data-alarmId');
Next, we check to see if the Alarm API is supported, and if so, use the mozAlarms.remove();
method to remove that particular alarm:
if(navigator.mozAlarms) { navigator.mozAlarms.remove(dataAlarm); }
Then we remove the list item containing the button that was clicked on, using this rather convoluted line:
event.target.parentNode.parentNode.removeChild(event.target.parentNode);
Last of all, we delete the item with that task title out of the IndexedDB, then write a message out to tell the user that task was deleted:
// open a database transaction and delete the task, finding it by the name we retrieved above var request = db.transaction(["toDoListAlarms"], "readwrite").objectStore("toDoListAlarms").delete(dataTask); // report that the data item has been deleted request.onsuccess = function(event) { note.innerHTML += '<li>Task \"' + dataTask + '\" deleted.</li>';
Permissions for the Alarm and Notification APIs
Please note that while the Alarm and Notification APIs is not privileged or certified, you should still include permissions
and messages
entries in your manifest.webapp
file when including them in an installable open Web app.
"permissions": { "desktop-notification" : { "description": "Required to schedule notifications" }, "alarms": { "description": "Required to schedule alarms" } }, "messages": [ { "alarm": "/index.html" }, { "notification": "/index.html" } ]