Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
November 15, 2021 10:15 am GMT

A beginner's guide to Javascript development using Firebase V9. Part 2 - coding a simple webapp

Introduction

A previous post, (Firebase V9. Part1 - project configuration), described the steps you need to follow to get yourself to the point where you could start coding. Here, at last is your chance to write some javascript - you've certainly earned it!.

If you've read my initial "Jungle" post, you may have written some code already. Remember this?

<p id="test">Hello there</p><script>let hourOfDay = (new Date()).getHours(); // 0-23if (hourOfDay < 11) {    document.getElementById('test').style.color = "blue";} else {    document.getElementById('test').style.color = "red";}</script>

Copy this over the content of index.html in the public folder, rerun the deploy command and refresh the https://fir-expts-app.web.app tab - the screen should now display "hello" in an appropriate colour, depending on the time of day.

Yes, it's that easy! But don't get over-confident - there's a lot still to learn!

What I'm going to do now is introduce you immediately to the Firebase functions that read and write data from a Firestore database. The example I've chosen is a simple "CRUD" (create, read, update and delete) application that will show you the basics. It's a good old-fashioned "shopping list" maintenance script.

When the shopping list webapp runs it's going to display a screen along the following lines:

Shopping List screen

I know this isn't going to win any trophies for quality user-interface design, but please bear with me - I'm trying to keep things as simple as possible so that we can concentrate on the Firebase issues. However, if you were to give my code a try, you'd find that it does work. User [email protected] could run this script to pull down a current copy of their shopping list, insert a new item with the "Add Item" button and amend and remove existing items with the Add and Delete buttons.

The way I'm approaching the design for this webapp is to use an index.html file to lay out a skeleton for this screen. Here's the <body> code.

<body style="text-align: center;">    <h2>Shopping list for :        <span id="useremail"></span>    </h2><br>    <div>        <!-- [userPurchase] [update button] [delete button] to be added dynamically here-->        <span id="usershoppinglist"></span><br><br>        <input type='text' maxlength='30' size='20' id='newpurchaseitem' autocomplete='off' placeholder='' value=''>        <button id="additembutton">Add Item</button>    </div>    <script type="module" src="index.js" async defer></script></body>

You'll notice immediately that quite a few things are missing from this code. For a start, there's nothing in the code for the Shopping list for : header identifying the owner of the list - just an empty <span> with a useremail id. Likewise the content of the shopping list block is identified but not specified. How is this ever going to work?

The information we need here exists in a Firestore database but can only be displayed when we retrieve it. So, we're going to make this work by adding some logic to our system - a bunch of javascript code that can be started up when the html file is loaded and that will perform the necessary database access tasks as required. Once the code has done its job, we can use the techniques that were first introduced in the original "Jungle" post to "insert" the missing html into the screen skeleton.

You might wonder, if I'm generating html code in javascript, why I bother with the html skeleton at all - why not just generate everything inside the <body> tags? The answer is that the skeleton is a great way of documenting the "structure" for your code. When html is generated inside javascript you'll find its clarity is seriously compromised and you start to lose track of the overall design. By contrast, when the design is defined in raw html, neatly indented and highlighted by the code-formatting tools in your IDE, it's much easier to see what's going on. I find it helpful to add "code comments" too, documenting the intended structure for any "missing" bits

Another difference between the code I'm showing you now and the examples I've used so far is that I'm no longer coding the javascript directly inside the html file. Instead, there's a <script> entry that simply refers the browser to an independent index.js file. This paves the way for use of special performance features introduced by Firebase 9. Note that the type of the script is declared to be module - more on this shortly.

In passing, I'd just like to mention that this style of development, wherein html code is dynamically-generated by javascript code is the hallmark of "single-page app" architectures, a term first introduced above in the context of firebase initialisation using the CLI. In the past it would be common for an application to present its users with an array of options laid out as tabs at the top of a screen display. The usual practice was to develop the code associated with each tab as a separate html file. The tabs would then be implemented as buttons each specifying an onclick referencing the file that was to be opened. This arrangement made life complicated for the developer, however, and the use of javascript in the application has opened up the opportunity to keep the code together in a "single-page app". See What is a single-page app for further background.

Simple Firebase CRUD code

As you've seen, when the shopping list webapp runs, the first thing that it needs to do is to display the current shopping list content. I've said that we're going to get this from a Firestore database so it's time for you to see what one of these looks like. In this section we're going to start by creating a database.

The data structure I have in mind for this application might go something like the following:

Test data structure

Here, the "shopping list" data just consists of pairs of email addresses and purchase items. The idea is that the system should permit many different users to share the same database - the email fields will allow us to keep their shopping lists separate. If things take off, perhaps we'll have millions of users!

In Cloud Firestore's NoSQL data model, data is stored in "documents" that contain fields mapping to values. These documents in turn are stored in "collections". A database thus consists of a set of collections inside each of which data is stored in documents.

The modelling of data structures and the design of databases to hold them is an extremely important aspect of system design, well beyond the scope of this simple introduction. Suffice to say that the facilities provided by Google within the Firestore framework are a sophisticated response to the requirements of modern IT applications. You might find it useful to use the web to read around the subject - Why successful enterprises rely on NoSQL might be a good place to start.

One important element of data modelling is the identification of "keys" - data fields that can be used (generally in combination) to uniquely identify documents. Often there's a natural key - for example "city name" in a collection of documents describing the characteristics of individual cities. Annoyingly, in our userShoppingLists collection, there isn't a natural key - but this is quite commonly the case and so you'll not be too surprised to find that Firestore is happy to generate artificial keys automatically in this situation.

Much of Google's Firestore documentation

Actually, I've chosen this example precisely because its documents don't have a natural key (much of Google's Firestore documentation decribes cases where a single field provides a natural key - something that in my experience is really quite unusual) and so my example pushes Firestore a bit harder. Firestore code for the two cases (natural key v generated key) is slightly different, the generated key form being a bit more complicated. But the advantage of using automatically-generated keys is that this approach can be used in all situations and so your code can follow a single style.

It's time now to go back to the Firebase console for our webapp project. Select the "Firestore Database" tool from the column on the left and proceed to initialise the database.

After a certain amount of preamble during which you specify a starting mode for security rules (select test for now - we'll put things on a production level later) and select a geographical location for the google servers that will hold your data (for UK users, anything starting with eu will be fine for a test development). Click "done" to "provision" your database and reveal the Firestore "collections management page" for the project.

It has to be said that the "management page" is a seriously tedious way of entering test data, but the screen works pretty well for the basic task of specifying and structuring collections in the first place. I don't think I can significantly improve on Google's documentation for this procedure, so I'll simply refer you to Managing Firestore with the console at this point. Try to create a collection called userShoppingLists for the data shown above. Remember that I have said that documents in the userShoppingLists collection should use automatically-generated keys. You should end up with something like the following:

Firestore Collections Management Screen

Those curious code in the userShoppingLists column are the automatically-generated keys for individual shopping list entries.

Right, with all this preamble concluded, let's concentrate on the application logic and the Firebase code located in the index.js file. Here it is:

// see https://firebase.google.com/docs/web/setup for latest browser modules source refimport { initializeApp } from 'https://www.gstatic.com/firebasejs/9.4.0/firebase-app.js';import {    getFirestore, collection, query,    getDocs, where, orderBy, addDoc, doc, updateDoc,    deleteDoc} from 'https://www.gstatic.com/firebasejs/9.4.0/firebase-firestore.js';const firebaseConfig = {    apiKey: "AIzaSyAPJ44X28caId8c3brUDM6FnKK5vQje6qM",    authDomain: "fir-expts-app.firebaseapp.com",    projectId: "fir-expts-app",    storageBucket: "fir-expts-app.appspot.com",    messagingSenderId: "1070731254062",    appId: "1:1070731254062:web:1e21b61bd95caeacdbc2bf",    measurementId: "G-Q87QDR1F9T"};const firebaseApp = initializeApp(firebaseConfig);const db = getFirestore(firebaseApp);const email = "[email protected]";window.onload = function () {document.getElementById('useremail').innerHTML = email;document.getElementById('additembutton').onclick = function () { addShoppingListDocument() };displayShoppingList(email);}async function displayShoppingList(email) {    // retrieve the shoppingList documents for email and turn them into entries     // in an editable Shopping List table    let userShoppingList = "";    const userShoppingListsCollection = collection(db, 'userShoppingLists');    const userShoppingListsQuery = query(userShoppingListsCollection,        where("userEmail", "==", email), orderBy("userPurchase", "asc"));    const userShoppingListsSnapshot = await getDocs(userShoppingListsQuery);    userShoppingListsSnapshot.forEach(function(doc) {        userShoppingList += `        <input type='text' maxlength='30' size='20' id='o` + doc.id + `' autocomplete='off'            placeholder='` + doc.data().userPurchase + `'            value='` + doc.data().userPurchase + `'>            <button id =  'e` + doc.id + `'>Update</button>            <button id =  'd` + doc.id + `'>Delete</button><br>            `;    });    document.getElementById('usershoppinglist').innerHTML = userShoppingList;    userShoppingListsSnapshot.forEach(function (doc) {        document.getElementById('e' + doc.id).onclick = function () { updateShoppingListDocument(doc.id) };        document.getElementById('d' + doc.id).onclick = function () { deleteShoppingListDocument(doc.id) };    });}async function updateShoppingListDocument(id) {    // update the userPurchase field for document id    let newUserPurchase = document.getElementById("o" + id).value    const docRef = doc(db, 'userShoppingLists', id);    await updateDoc(docRef, { "userPurchase": newUserPurchase });}async function deleteShoppingListDocument(id) {    // delete the document for document id    const docRef = doc(db, 'userShoppingLists', id);    await deleteDoc(docRef);    displayShoppingList(email);}async function addShoppingListDocument() {    // add a new document    let newUserPurchase = document.getElementById("newpurchaseitem").value;    const docRef = await addDoc(collection(db, "userShoppingLists"), {        "userEmail": email,        "userPurchase": newUserPurchase    });    displayShoppingList(email);    document.getElementById("newpurchaseitem").value = '';}

The script starts with a bunch of import statements. Firebase 9 delivers its library code to the application via "modules", one for each major function group (eg "authentication"). When we import one of these, we must also declare the component functions that we want to use - the aim being to minimize the size of the application.

One consequence of using module import statements in a script is that a javascript file that contains them itself becomes a module - more on this later.

Because in this post I want to concentrate on the essentials of Firestore coding, I've chosen to use the "browser module" form of the Firebase modules - files with an https:// address drawn down at run time from the web. In a production application, you'd use modules that you first draw down into your terminal environment and which you "package" into your javascript using a tool like "webpack" prior to deployment. This is more efficient, but since efficiency isn't an issue just now and deploying your project when you use "proper" modules adds complications (because browsers don't understand these) I've chosen to avoid this complication just now. So, "browser modules" it is.

Immediately after the import statements we get our first sight of a firebase function in action - an initializeApp() call that will give our webapp (running in our browser) a db object linking it to our database (sitting out on the web in the Google cloud). This link is delivered with reference to a firebaseConfig json supplying all the necessary keys (see Eloquent Javascript for a description of the json format). The contents of this json were defined when we created our Firebase project and can be found by opening the Firebase console for the project and clicking the gear wheel icon to view the project properties. I got these into my index.js file by simply copying and pasting.

Once the webapp has successfully created its db object, it's free to do anything it likes with this database. We'll talk about the security implications of this later, but for now let's just concentrate on applying this new-found freedom and using it to read a shopping list!

If you scan down the remainder of the code you'll see it consists largely of four functions, one for each of the four CRUD operations. The first thing to note is how compact the codet is. For example, the deleteShoppingListDocument(id) function used to delete a document with id id from the userShoppingLists collection is just three lines long (and one of those is not strictly anything to do with the deletion process because it simply refreshes the screen to confirm the successful completion of the deletion operation). This, I suggest, is a modern miracle - in the past, such functions would have used sa whole bunch of complicated javascript calling an equally sophisticated piece of PHP code (or similar host-based language) stored in a separate file and hosted on a separate device.

Sticking with the deleteShoppingListDocument(id) function, note that the core of this is a call to a deleteDoc() function preceded by an await keyword (an extension added to the javascript language only relatively recently). I spent some time in my previous "Jungle" post describing the "asynchronous" nature of javascript calls to file IO (input/output) functions. This is an example. In normal circumstances, a deleteDoc() call will certainly initiate the necessary deletion action, but control flow in the program making the call will pass immediately to the next statement - ie, without waiting for the deleteDoc() result. In the present case, unless we take some special precautions, the displayShoppingList(email) in the next statement might well simply show an unchanged display (because the deletion hasn't taken place yet)

However, in the case of this particular piece of code, we've used the await keyword. As a result, control doesn't reach the screen refresh call until the deleteDoc() has finished. Note that a call to deleteShoppingListDocument() itself won't wait for a result though. You still need to keep your wits about you when you're working with asynchronous operations!

Note also that in order to use the await keyword we have had to declare the parent deleteShoppingListDocument(id) function as asynch.

I'm not going to go into detail about the precise form of the individual Firestore functions used to perform the CRUD operations - I think it's best if I refer you to Google's documentation at Add data to Cloud Firestore. But there's one wrinkle that I do want to mention.

If you look at the code for the additembutton button in the index.html file, you'll see that it doesn't specify what's to happen when the button is clicked. Normally I'd have done this by including an onclick = clause to direct the button to the appropriate CRUD function. While this is an arrangement you might have used freely in the past with "ordinary" scripts, I'm afraid that we have to do things differently when we're using modular scripts.

In this case, if you tried the conventional approach, when you clicked the button you'd find that your program would tell you that "your onclick function is undefined". What? But it's there - in the script!

Well it might be in the script, but the script is declared as type module (it has to be in order to enable us to use the import keyword to load our Firebase api functions) and the "namespace" for a module (ie the collection of variable and function names referenced in the script) are only available to that module. In particular, they're not available to the DOM. This arrangement is designed to ensure that modules don't interfere with each other (ie so they're 'modular').

What we have to do is add the onclick to the button dynamically in the module once the DOM has loaded. So if you back at the code for index.js you'll see that one of its first actions is to launch the following statement:

document.getElementById('additembutton').onclick = function () { addShoppingListDocument() };

This completes the setup of the button and allows us to use it in the DOM.

Things get a little more complicated in the displayShoppingList() function where we dynamically generate html to display complete buttons alongside the <input> items on which they are to act (and note, in passing, how confused the html code specification is here - perhaps you'll see now why I was concerned to use the index.html file to define the layout aspect of the webapp). In this case you might think we could generate a button complete with its onclick specification all at the same time. But if you tried this, having inserted the code block into the DOM with the

document.getElementById('usershoppinglist').innerHTML = userShoppingList;

instruction, you'd find that your new buttons failed in exactly the same way as previously described. What we have to do is first generate the code without the onclick specification, update the DOM and then add the onclicks. This explains the second

    userShoppingListsSnapshot.forEach(function(doc) {

loop in the displayShoppingList() function's code.

This is a nuisance, (entirely consequent on Firebase Version 9's move to a modular approach) but a small price to pay for the gains one obtains elsewhere through the use of the Firebase api.

Now that I've homed in on the forEach structure, I think I should also say a bit about this too. "Queries" are used to get "snapshot" subsets of the documents in a collection in response to a specification of selection and sorting criteria. They're documented at Querying and filtering data .

Once you've got a snapshot, the foreach construct allows you to work your way through all the documents that it contains. For each doc, you have access to both its data items (as doc.data()."item name") as well as the document id itself (as doc.id). In this particular instance I use the document id as a convenient way of applying an identifier to the <input> and <button> elements and supplying parameters to their onclick functions.

Something else you should know about queries is that they will almost always need to be supported by an index (ie a quick way for Firestore to check which documents match selection criteria without reading them the whole collection). The data tab in the Firestore Database tool gives you a method of creating indexes, but you might actually find it easier just to let your queries fail and pick up the consequences in the browser system tool. This is because the error announcing such a failure will include a helpful link that, when clicked, will create the index for you. This is a seriously useful arrangement. Thank you Google!

In summary, there are quite a few other "wrinkles" to using firestore functions on complex data structures, but overall, I'll think you'll find that everything works pretty smoothly. My own experience has been overwhelmingly positive - a huge improvement over the technologies I've used previously.

Adding a login and securing the database from unauthorised access

But we can't relax just yet. There's still a large hole in the functionality of this webapp because, when we initially configured our database, we created it as a "test" deployment. Currently we're connecting to our firestore database by referencing our firebaseConfig data item with all its apikeys etc. Anybody skilled in the use of browser tools will be able to read this from our webapp and there's nothing at present to stop them copying this to their own webapp and thus gaining access to our database.

Rather than trying to hide the firebaseConfig item (a fruitless task), Google provides a cloud-based arrangement, stored within our Firebase project and thus accessible only to us (via our Google account), that allows us to specify the tasks (read, write etc) that can be performed against specified criteria (eg "user logged into our project"). What I mean by "logged in" in this instance means "having presented a user id and password that matches the settings for a table of users also defined in our Firebase project". So, it's time to look at adding a login function to our webapp.

The Firebase arrangements for protecting our database are defined using "rules" that we define using a simple coding system in the Firebase Console for our project.

If we select the Firestore Database tool on the console and click the rules tab, we'll see the current rule specification. At this stage this will still be set to the initial "test" state and will look something like the following:

service cloud.firestore {  match /databases/{database}/documents {    match /{document=**} {     allow read, write: if true;    }    }}

This is basically saying "allow everybody both read and write access to everything". Only firestore apis are permitted to access firestore cloud data and every firestore api call (eg, deleteDoc()) asked to perform a read or write operation on a document will first inspect the project's rules to see whether or not that proposed action is permitted. While our rules remain as above, the api calls will allow everything.

In our case, what we want is to arrange firstly that documents are only available to "logged-in" users and secondly that those users can only see documents stamped by their user-id (userEmail. The rule specification in this case needs to be changed to :

service cloud.firestore {  match /databases/{database}/documents {    match /userShoppingLists/{document} {        allow read, write : if request.auth != null && request.auth.token.email == resource.data.userEmail;    }  }}

See Google's documentation at Basic Security Rules for a description of the rules-specification language - a wonderfully powerful and flexible arrangement. At the same time, however, it has to be said that the language can be difficult to work with. Fortunately the specification tab comes equipped with a "playground" that allows you to check out the validity of your rules before you publish them (ie, apply them to the live database).

So far so good. But once your rules are updated as indicated above published, you'll find that your app won't work any more. If you "inspect" the code in the browser, you'll see that your database access commands are being rejected with "insufficient privilege" messages. THe problem of course is that the rules have now been set to allow database access only to users who are "logged in". How do your users get to be "logged-in"?

The short answer is "by using one of the methods that Firebase provides to log them in".

Quite the easiest way to achieve this (since we're using Google services ourselves) is to accept users as logged in if they're logged in with Google. To do this we simply replace the temporary email = "[email protected]"; "fudge" setting a fixed email address, together with the window.onload function that launches the app by the following:

const provider = new GoogleAuthProvider();const auth = getAuth();var email;window.onload = function () {signInWithPopup(auth, provider)    .then((result) => {        // This gives you a Google Access Token. You can use it to access the Google API.        const credential = GoogleAuthProvider.credentialFromResult(result);        const token = credential.accessToken;        // The signed-in user info.        const user = result.user;        email = user.email;        document.getElementById('useremail').innerHTML = email;        document.getElementById('additembutton').onclick = function () { addShoppingListDocument() };        displayShoppingList(email);        // ...    }).catch((error) => {        alert ("Please sign into your Google a/c to proceed");    });}

We also need to add a new import statement at the top of the code to draw in the new GoogleAuthProvider, signInWithPopup functions we're going to reference:

import { getAuth, GoogleAuthProvider, signInWithPopup } from 'https://www.gstatic.com/firebasejs/9.4.0/firebase-auth.js';

Finally, to authorise Google login as a valid way of accessing the webapp, we need to click the "Signin-in-method" tab for the Firebase console's Authentication tool and enable Google as a "permitted sign-in provider".

If you now redeploy the webapp, you'll find that it displays a popup window that checks for the existence of a logged-in Google account on your device. If it finds one, the popup disappears and the application displays the shopping list for the logged in email. If it can't find one, the popup asks you to log in with one. Neat - this is seriously powerful IT and a great saver of development effort!

If the account used to access the webapp is new to the project (in which case, of course, the webapp will display an empty shopping list, ready for the user to add purchase items), logging in also adds the account id to the Firebase console's list of application users for your project (thus allowing you to keep track of who's using it). You'll find this list under the Users tab of the Console's Authentication tool

Recognising that not everybody wants to use Google sign-in for authentication, Firebase offers numerous alternative sign-in providers such as Twitter and Facebook. But if you want to be a bit more conventional and customise your own arrangements for registering users, Firebase functions are available for this as well. You can see an example of this arrangement in the bablite.web.app pilot referenced earlier. Just start it up in the browser and "inspect" its index.js code to see how customised registration is achieved..

Google's documentation for the various signon methods can be found at

What else is there to say?

If you've been following this post just to try out the technology, you can give yourself a pat on the back and stop now - you've seen a seriously useful application, advertised on the web and secured from malicious activity.

But suppose you wanted to put this on a production basis with real users - perhaps users who are paying you for the privilege of using your app? In such a case you might want to look at the firebase emulator.

The firebase emulator: Want to make some changes to your code? How do you do this without upsetting your users while you test the changes? What you need is somewhere else to source the webapp and perhaps another database as well. The firebase emulator allow you to run your webapp from files on your own machine and, if you choose, run it against a local Firebase database. This sounds as if this might be rather difficult to arrange, but actually the firebase design makes it really straightforward by providing an "emulator" system. Once you've installed the emulator you'll find you have access to exactly the same facilities that you enjoy in the live Firebase console. It's easy to install and operate too.

If you've got a serious production webapp and want to keep ahead of the competition , you may also be concerned about efficiency. If you want your product to be "lean and mean" you need to look at the "tree-shaking" arrangements that Firebase 9 offers.

Webpack and "tree shaking": Google has really pulled out all the stops in version 9 to ensure that the code it produces meets the latest expectations for efficiency and resilience. Sadly, because the procedure I've described thus far uses "browser modules" the code as described above can't take advantage of the new arrangements. But once again, the procedure is easier to apply than you might imagine. Basically, it just boils down to reverting the code to reference "proper" modules and using a terminal session to run ebpack -a third-party piece of software - to produce a "compressed" version of the initial index.js file.This is then deployed in its place. It's really just a question of getting your "workflow" organised. You might also want to consider version control issues and bring Github into the picture as well.

A large webapp will need to cover a lot of ground - you'll need to work hard to keep the code tight and maintainable. Firebase "functions" let you both organise the code and spread the processing load.

Firebase Functions and Background tasks: It makes sense to configure certain elements of your application's operations as background events. An example might be the despatch of an email when a user signs up for a new account. Situations like this will arise in many different situations and, since these actions are generally "secondary" to the main purpose of their parent transaction, it makes sense to handle them as background tasks. Firebase "functions" enable us to code these background tasks in javascript and launch them in response to trigger events fired by their parent transactions.

There's a lot more to Cloud Services than Firestore databases. You may find you have a need for hosted "conventional" storage.

Cloud storage: How would you use your webapp to upload a conventional file into the Google cloud and read it back once it arrives there? Cloud Storage is available to provide an answer to this and any other storage requirements that don't conveniently fit into the database collection structures we've seen so far.

However, I think you've suffered enough for now. But watch out for future posts in this series.


Original Link: https://dev.to/mjoycemilburn/a-beginners-guide-to-javascript-development-using-firebase-v9-part-2-coding-a-simple-webapp-4e5i

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To