NgSysV2-2.3: Creating a simple Svelte Information System with Google's Firestore

mjoycemilburn

MartinJ

Posted on November 26, 2024

NgSysV2-2.3: Creating a simple Svelte Information System with Google's Firestore

This post series is indexed at NgateSystems.com. You'll find a super-useful keyword search facility there too.

Last reviewed: Nov '24

1. Introduction

Most webapps exist purely to create and access shared information. Consider, Amazon's https://www.amazon.co.uk/ website. The essential purpose of this system is to let you browse a central collection of product details, place orders and monitor progress on delivery. To make this work, Amazon must:

  • Maintain this information somewhere accessible over the web
  • Structure and manage it to ensure near-instant access and total integrity.

This post is all about the "database" technology used to achieve these aims.

Warning - this is a long post because database reads and writes in Svelte draw you remorselessly into using SvelteKit's client-server architecture. Previously, your code ran exclusively "client-side" in your web browser. Now you'll also be running code on the local server launched by npm run dev. This has consequences...

I've looked at ways of splitting the post up but they don't work. To make matters worse, the Javascript you'll use contains many new features. So, I'm sorry - you're just going to have to suck it up.

But look on the bright side. Once you're through this, things will start to get easier. Take it slowly. Use chatGPT where you feel I've not explained something clearly. You'll find the bot particularly helpful when you need advice on JavaScript syntax. Relax. This is going to be fun!

2. Configuring your project to use Google's Firestore

There are innumerable ways of storing shared data on the web. This post series uses Google's Firestore system because it suits beginners. It requires minimal setup and fits comfortably within the structure of a Svelte webapp.

You'll need to perform four initial steps:

  1. Obtain a Google account
  2. Create a Firebase project under this account
  3. Register your "webapp"
  4. Initialise a Firestore Database for your Firebase project

Firebase is Google's umbrella term for many different services you might use to mount a simple project on the web. The services for a given account are managed via Google's "Firebase console" at https://console.firebase.google.com/. They include both a "Storage" service that enables you to upload files into the Google Cloud and a "Firestore Database" service. A database differs from a file in that it possesses a configurable structure. It enables you to access and update discrete elements of the configured data set.

2.1 Obtaining a Google account

If you have a Gmail address, you're already covered because this automatically counts as a Google account. If you don't, follow the instructions at Create a Google Account to get one.

2.2 Creating a Firebase project for your code

Launch the Google Firebase console and log in with your Google Account (noting that, if you're logged into Gmail with this, you're already logged into the Firebase Console as well). Now click the "Create a Project" box to launch the process.

Google will want you to supply a name for your project (I suggest you use the project name you're using in VSCode), and will propose an extension that makes this a unique "Project Identifier" within the Firebase world. So for, example, my version of the "Svelte-dev" project used in this post series is known to Google as "Svelte-dev-afbaf".

As an aside, since the Project Identifier will ultimately form part of the default live URL for your webapp, and since Google lets you edit its initial "uniqueness extension" proposal, you might be tempted to try to change this to something meaningful. However, I suggest you forget this idea. Firstly, you'll find it difficult to pick an identifier that suits both parties. Secondly, in my experience, these "default URLs" aren't ever indexed by Google. A "custom URL" purchased at minimal cost and linked to your default URL when you go live is by far the best way to get a memorable URL.

Now click "Continue" to reveal a "Google Analytics" registration page. You can safely ignore this here as it is relevant only to performance issues on live apps. Use the slider bar to decline it and click the "Create Project" button to continue.

The lights now dim a little as Google registers your project. Finally, once you've clicked one more "Continue" button, you'll find yourself in your project's Firebase Console window. Here's a screenshot of the Firestore tab for a "svelte-dev" project:

Illustration of Firebase console interface

It's worth giving yourself a moment to familiarise yourself with this page because it is a little complicated. The basic structure consists of a "tools menu" on the left that determines what gets displayed in the main panel on the right. The problem is that the menu is "adaptive" and maintains a "Project shortcuts" section that remembers where you've been. Consequently, the menu seems to look different every time you open the console! Once you've grasped this feature, however, things work well. Note that the complete set of tools is hidden within the "Build", "Run" and "Analytics" submenus of their parent "Product Categories" menu item. The "Build" set contains everything you need at present.

Before you proceed further, note the following:

  • information at the top of the screen confirms that the svelte-test project is currently registered on the "Spark" plan. This means that everything you're doing at present is free. Eventually, in this post series, you will get to a point where you need to start to paying Google, and will need to upgrade your project to the "Blaze" tariff. But don't worry - this is a long way off yet, you won't pay much, and you can create a monthy budget to limit the amount Google might try to charge you.
  • project details are revealed by clicking the "Project Overview" button at the top of the toolbar. Details available here include a reminder of the Project Identifier and a button to delete the project. If everything goes wrong, you can always use this to rub out the mess and start over again. This won't cost you anything

2.3 Registering your webapp

Firebase needs to know what your webapp will be called:

  • Click the </> icon below the "Get started" message and supply a nickname when requested. I suggest you use your project name again here (eg "svelte-dev").
  • Ignore the offer to "Set up Firebase Hosting for this app" because, when we finally get around to deployment, all your hosting needs will be handled by App Engine.
  • Finally, click "Register" and "Continue to the console" to return to the initial console screen.

2.4 - Initialising a Firestore database

Select "Firestore Database" from the "Build" stack in the tools menu to obtain the Firebase console view shown below:

Graphic showing Firebase console database-initialisation screen

Once you've clicked the "Create Database" button, the console will want you to:

  1. Set your database "Name and Location". "Name" is the identifier for the database and will only be relevant if you plan to create several different databases in your project. Leave this empty, for now, so that Google uses its "default" setting. "Location" specifies where your database will be physically located. The pull-down list of options available here is possibly your first sight of the scale of the Google Cloud service. Its server farms are available right around the globe. You will probably want to select a server close to your location. For example, I generally use "europe-west2 : Heathrow" since I am based in the UK. Pages elsewhere in the Google Cloud console enable you to specify performance and availability characteristics, but you don't need to look at these for now.

  2. Secure your database with "Rules". The screen here offers you a choice between setting initial "production" and "test" "rules". This only makes sense, of course, if you know what "rules" are in the first place - and this isn't the right time for me to explain them. Unless you know better, I'd like you to check the "test mode" option here. Be assured, I'll come back to this later when I talk about "authorisation" (and, oh boy, is there a lot to talk about!).

Once you're through these two stages, the Cloud Firestore page opens in the Firebase Console. What now?

3. Working with a Firestore Database

This section aims to answer the following questions:

  1. What is a database?
  2. What does a Firestore database look like?
  3. How can I initialise a database in the Firestore console?
  4. How can I access a Firestore database in Javascript?
  5. How can I get a Svelte +page.svelte file to load data from a Firestore database?
  6. How can I get a Svelte +page.svelte file to add data to a Firestore database?

3.1 What is a database?

For our immediate purposes, a database is a set of tables containing rows of values for named data "fields". So, for example, a Customer Order" database might contain

  • a "Customers" table full of "Customer Id" and "Customer Address Details" field values
  • a "Products" table full of "Product Number" and "Product Detail" fields
  • a "Customer Orders" table with details of the orders for a "Product Number" placed by a "Customer Id"

The important thing is that such an arrangement is structured with consistent standards for the naming and formatting of data content

3.2 What does a Firestore database look like?

In Firestore, tables are called "collections" and rows within them are called "documents". Documents stored within a collection aren't all required to have the same fields, but field names and content are expected to follow consistent patterns throughout the collection.

An important feature of a Firestore document is that it should have a unique identifier or "key". Sometimes there will be a field such as "Email Address" within each document that you can use to provide a "natural" unique key. If not, Firestore can be asked to create an artificial key automatically.

Database design is probably the most challenging part of system development and, once again, I will duck this because the issues involved will only become clear when you've had some practical experience. However, when you have a moment, you will find it useful to check out the Cloud Firestore Data model page.

3.3 How can I initialise a database in the Firestore console?

In this post, I plan to show you how to create a single products collection in your default Firestore database. This will contain simple documents containing a product number field with a key automatically generated by Firestore.

On the Firestore page on the Firebase console, click the "Start collection" button and enter the name "products" in the "Collection ID" field of the popup that now appears.

Graphic showing Firebase console collection-initialisation screen

Now use the data entry page to create a test products document containing a "productNumber" field with a numeric value of "1" and a "productDetails" field with a text value of "Product 1".

  • Type "productNumber" in the "Field" entry box to set the field name, set the "Type" box to "number" and enter "1" (without the quote characters) in the "Value" box.
  • Click "add field and type "productDetails" in the "Field" entry box to set the field name, leave the "Type" box set to its default "string" setting and enter "Product 1" (without the quote characters) in the "Value" box.

Now sign off the document by first clicking the "Auto Id" button and then "Saving" it Here's what the console should look like now:

Graphic showing Firebase console document-display screen

If you wanted to add further documents, you would click "add document" at this point, but that's not necessary in this case - you only need a single document to demonstrate your webapp's ability to read it.

You're finished here for now, but note that the console's "panel view" lets you edit or delete the document you've just created. If you've got in a complete mess you can even delete the entire collection and start again.

3.4 How can I access a Firestore database in Javascript?

Here's where things start to get really interesting!

Google provides a library of Javascript functions to let you read and write Firestore documents. Such libraries are referred to as "APIs" (Application Program Interfaces). Have a look at the following code that shows how the firebase/firestore library would be used to read all the documents in svelte-dev's products collection:

import { collection, query, getDocs, orderBy } from "firebase/firestore";
import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";

const firebaseConfig = {
    apiKey: "AIzaSyCE933 ... klfhFdwQg1IF1pWaR1iE",
    authDomain: "svelte-dev-afbaf.firebaseapp.com",
    projectId: "svelte-dev-afbaf",
    storageBucket: "svelte-devt-afbaf.appspot.com",
    messagingSenderId: "1027 ... 85697",
    appId: "1:1027546585697:web:27002bf ..... b0f088e820",
};

const firebaseApp = initializeApp(firebaseConfig);
const db = getFirestore(firebaseApp);

const productsCollRef = collection(db, "products");
const productsQuery = query(productsCollRef, orderBy("productNumber", "asc"));
const productsSnapshot = await getDocs(productsQuery);

let currentProducts = [];

productsSnapshot.forEach((product) => {
    currentProducts.push({productNumber: product.data().productNumber});
});

return { products: currentProducts } // accessed in +page.svelte as data.products
Enter fullscreen mode Exit fullscreen mode

Concentrate on the section that starts const productsCollRef = collection(db, "products");. This uses Firestore API calls to load a sorted copy of all documents within the products collection into the State currentProducts variable.

First, the collection and query functions, drawn from the Firestore Client API library, are used to point Firebase at the products collection and define a query to run on it. Then the query is launched by a getDocs API call.

I won't attempt to describe the mechanics of this sequence of Firestore API calls. Treat these as a piece of "boiler-plate code" - code - the sort of thing that you write once and, ever afterwards, simply copy. Since you'll find you need a whole library of templates to cover the full array of Firestore "read", "update" and "delete" operations, you might find it useful to look at "Firestore CRUD command templates" in Post 10.1. If you'd like to check out Google's own description of the API, you'll find links to these at the end of Post 10.1.

"CRUD" here is short for "create", "read", "update" and "delete".

The getDocs result is returned as an array of documents conventionally called a "snapshot". However, note that the getDocs function call is preceded by an await keyword.

The await keyword is needed here because, by default in Javascript, instructions referencing external data sources that may take an unpredictable time to complete are handled asynchronously. The "await" keyword essentially (though this is a gross simplification) enables you to override this arrangement. When you have more time, you might find it useful to have a look at A simple guide to the Javascript fetch() API and the "await" keyword

But right now, returning to our code snippet above, look at the section starting with the const firebaseConfig statement.

The firebaseConfig declaration initialises an object containing the configuration details needed to connect your web app to your specific Firebase project. It includes various keys and identifiers that Firebase uses to locate and authenticate your app. You'll find the settings for your particular webapp in "Project Overview/Project settings" on the Firebase console. The firebaseConfig settings in the code samples below have been "obfuscated" because they are unique to my project and aren't to be passed around lightly (more about this shortly). When trying the sample code below, you'll need to copy in the firebaseConfig settings from your own project.

With firebaseConfig initialised, the webapp can initialise the db variable required by the query's const productsCollRef = collection(db, "products"); statement:

const firebaseApp = initializeApp(firebaseConfig);
const db = getFirestore(firebaseApp);
Enter fullscreen mode Exit fullscreen mode

Finally, you may be wondering where these API functions come from. The answer is that they are imported from locations in your project by the three statements at the top of the code block:

import { collection, query, getDocs, orderBy } from "firebase/firestore";
import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";
Enter fullscreen mode Exit fullscreen mode

Here "modular libraries" are being accessed to supply functions for your code. Named functions such as collection are extracted from a parent module to fulfil the references required later in the code.

But then this leads to the question "And how do modular libraries get into my project in the first place?" The answer, of course, is that you have to put them there, and the tool you use to do this is faithful old npm.

Back in a VSCode svelte-test terminal session (terminate the dev server if necessary with a couple of ctrl-C keystrokes) and run the following instruction'

npm install firebase
Enter fullscreen mode Exit fullscreen mode

After a minute or two (the installation involves a sizeable download), you'll be poised to run code that downloads a Firestore database collection. But, you still don't know how to embed this in a Svelte webapp. So, on to the next question...

3.5 How can I get a Svelte +page.svelte file to load data from a Firestore database?

This has been a long haul but, hang in there, you're nearly finished.

Currently, in the <script> section of your src/routes/+page.svelte file, you have the following statement:

let products = [];
Enter fullscreen mode Exit fullscreen mode

As you know, this declares your products field as a State variable and initialises it as an empty array. What you want to do now is replace "empty" with the content of the products Firestore collection.

Unfortunately, as you've seen, this involves an asynchronous operation. This complicates things somewhat because Svelte doesn't want anything to slow down the initial load of a page - it's happy to see information added later but, the first user impression should be of an instantaneous response. Svelte has a standard arrangement for loading initial data into a +page.svelte file. It goes like this:

First, you create a new src/routes/+page.server.js file that wraps all your asynchronous code inside a load() function (mandatory name) and returns its results as an object.

Here's the code

// src/routes/+page.server.js
import { collection, query, getDocs, orderBy } from "firebase/firestore";
import { initializeApp, getApps, getApp } from 'firebase/app';
import { getFirestore } from "firebase/firestore";

export async function load() {

const firebaseConfig = { // replace with settings from your own project!!
    apiKey: "AIzaSyCE933v....vklfhFdwQg1IF1pWaR1iE", // obfuscated code
    authDomain: "svelte-test-afbaf.firebaseapp.com",
    projectId: "svelte-test-afbaf",
    storageBucket: "svelte-test-afbaf.appspot.com",
    messagingSenderId: "1027 ..c585697",
    appId: "1:1027546585697:web:27002bf  ...ccccb0f088e820", // obfuscated code
};

// see Post 3.3 for an explanation of the "ternary" expression used below
const firebaseApp = !getApps().length ? initializeApp(firebaseConfig) : getApp();
const db = getFirestore(firebaseApp);

const productsCollRef = collection(db, "products");
const productsQuery = query(productsCollRef, orderBy("productNumber", "asc"));
const productsSnapshot = await getDocs(productsQuery);

let currentProducts = [];

productsSnapshot.forEach((product) => {
    currentProducts.push({productNumber: product.data().productNumber});
});

return { products: currentProducts }
}
Enter fullscreen mode Exit fullscreen mode

The load function above returns an object with a single products property whose value is the currentProducts array constructed by the Firestore API calls.

This is all very well, but how is this to be conveyed to the products state variable in +page.svelte?

The first step is to advertise a new data (mandatory name) state variable as a prop (short for "property") of +page.svelteYou do this by declaring it with an export keyword, thus:

export let data;
Enter fullscreen mode Exit fullscreen mode

Props won't be covered in this series until you get to Post 3.1 in this series and learn about "components". For now, think of your +page.svelte file as a function with data as its parameter.

When you run your +page.svelte file now, the SvelteKit framework sees the export let data declaration with its reserved data keyword and thinks, "ah, I need to run the load() function associated with this page". The products data is duly returned into the data prop of +page.svelte and now, since this is a reactive variable, the page is refreshed.

All you need to do to make your existing "template" code work with the new arrangement is to replace products references with data.products

The +page.server.js file is your first sight of "server-side" code in Svelte - that is, code that runs in the server. All the +page.svelte code you've seen so far runs "client-side" in the browser. A +page.server.js file, by contrast, runs either in the local server launched by npm run dev or, after deployment, in the Node.js environment of an App Engine server. Server-side code runs faster than client-side code and is secure. The only person who can view or change it is you - its owner.

Here's the complete code for a modified version of the +page.svelte file from Post 2.2:

// src/routes/+page.svelte - remove before running
<script>
    let popupVisible = false;
    let newProductNumber;
    export let data; // An array of data.product objects returned by +page.svelte.js
</script>

<div style="text-align: center">
    <h1>Products Maintenance Page</h1>
    {#if !popupVisible} <!-- the start position - popup not visible-->

        <p>Currently-registered Product Numbers:</p>
        {#each data.products as product} <!-- display the current list of  products-->
            <p style="margin: 0">{product.productNumber}</p>
        {/each}
        <button
            style="margin-top: 1rem"
            on:click={() => {   // a function to toggle the popup on
                popupVisible = true;
            }}>Add Another Product
        </button>

    {:else} <!-- display the product registration form-->

        <form
            style="border:1px solid black; height: 6rem; width: 30rem; margin: auto;"
        >
            <p>Product Registration Form</p>
            <label>
                Product Number
                <input bind:value={newProductNumber} type="text" />
            </label>
            <button
                type = "button"
                on:click={() => { // a function to add the product number to the array and hide the popup
                    data.products.push({productNumber: newProductNumber});
                    popupVisible = false;
                    newProductNumber = ""; // Reset the input field
                }}>Register
            </button>
        </form>

    {/if}
</div>
Enter fullscreen mode Exit fullscreen mode

If you now replace the contents of your current src/routes/+page.svelte file with the above code, update thefirebaseConfig settings in +page.server.js with parameters from your own project), and start your dev server as before, the page should initialise with the single "Product Number 1" record you created in the products collection earlier. Adding new Products should also work exactly as before.

But there's a snag. When you restart the webapp you'll see that you're back to square one with a single "Product Number 1" record. Can you see the problem? Yes, the webapp is still updating the in-core list of products (now located in the new data.products variable). You need to find some way of replacing this with code that updates the products database. Onward and upward!

3.6 How can I get a Svelte +page.svelte file to add data to a Firestore database?

What you need is another piece of Firestore API boilerplate code - this time a sequence that creates a new {productNumber: newProductNumber} document in a Firestore products collection (to simplify things, I'm ditching the productDetails property for now). Here it is:

const productsDocData = {productNumber: newProductNumber}
const productsCollRef = collection(db, "products");
const productsDocRef = doc(productsCollRef );
await setDoc(productsDocRef, productsDocData );
Enter fullscreen mode Exit fullscreen mode

Where should this code be located? Currently, the code fired by the "Add Another Product" <form>'s on:click button lives in your +page.svelte file. But Svelte advises that, for security and efficiency reasons, the database update should be performed "server-side" in +page.server.js in an actions() function paralleling the load() function you've already created here. The function is triggered by "posting" data from the <form>.

Here's what the new <form> arrangement in +page.svelte should look like:

// src/routes/+page.svelte - fragment

<script>
    let popupVisible = false;
    export let form;
</script>
<form
    method="POST"
    style="border:1px solid black; height: 6rem; width: 30rem; margin: auto;">
    <p>Products Registration Form</p>
    <label>
        Product Number
        <input name="newProductNumber" />
    </label>
    <button type="submit">Register</button>
</form>
Enter fullscreen mode Exit fullscreen mode

The most important thing here is the presence of the method=POST qualifier on the <form> specification. When the form is submitted, this makes SvelteKit think "ah, I now need to pass the data collected by this form to actions() in a +page.server.js file associated with this page. Because no explicit "action" is requested above, SvelteKit goes looking for a "default" actions() function. Here is is

// src/routes/+page.server.js - fragment
export const actions = {
  default: async (event) => {

    const formData = await event.request.formData();
    // retrieve the newProductNumber string from the form and convert it to a number
    const productNumberAsText = formData.get('newProductNumber');
    const newProductNumber = parseInt(productNumberAsText, 10)
    console.log("newProducNumber : " + newProductNumber)

    const productsDocData = { productNumber: newProductNumber }
    const productsCollRef = collection(db, "products");
    const productsDocRef = doc(productsCollRef);
    await setDoc(productsDocRef, productsDocData);

    return ({updateSucceeded: true})

  }
};
Enter fullscreen mode Exit fullscreen mode

The object returned by the "actions" function becomes available to the +page.svelte file through the form prop declared in its <script> section. Here, an export let form; statement parallels the earlier use of a data prop to return the results of a load() function.

Here are the complete versions of +page.svelte and +page.server.js:

// routes/+page.svelte - remove before running
<script>
let popupVisible = false;
export let data; 
//The "data" State variable will be initialised by +page.server.js as an array of {productNumber : value}
// objects
export let form;

</script>

<div style="text-align: center">
<h1>Products Maintenance Page</h1>

{#if !popupVisible}
    <!-- the start position - popup not visible-->

    <p>Currently-registered product numbers:</p>
    {#each data.products as product}
        <!-- display the current list of products-->
        <p style="margin: 0">{product.productNumber}</p>
    {/each}
    <button
        style="display: inline; margin-top: 1rem"
        on:click={() => {
            popupVisible = true;
        }}
        >Add Another Product
    </button>
{:else}

    <!-- display the product registration form-->
    <form  method="POST"
        style="border:1px solid black; height: 6rem; width: 30rem; margin: auto;"
    >
        <p>Product Registration Form</p>
        <label>
            Product Number
            <input 
            name="newProductNumber" type="text"/>
        </label>
        <button type="submit">Register</button>
    </form>
{/if}

</div>
Enter fullscreen mode Exit fullscreen mode
// routes/+page.server.js
import { collection, query, getDocs, orderBy, doc, setDoc } from "firebase/firestore";
import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";

const firebaseConfig = {
  apiKey: "AIzaSyDOVyss .... LsT6n-tWh0GZoPQhM",
  authDomain: "svelte-dev-80286.firebaseapp.com",
  projectId: "svelte-dev-80286",
  storageBucket: "svelte-dev-80286.appspot.com",
  messagingSenderId: "5855 ...  06025",
  appId: "1:585552006025:web:e41 .....  fcc161e6f58"
};

const firebaseApp = initializeApp(firebaseConfig);
const db = getFirestore(firebaseApp);

export async function load() {

  const productsCollRef = collection(db, "products");
  const productsQuery = query(productsCollRef, orderBy("productNumber", "asc"));
  // Because the productNumber field in the products collection is numeric rather than string, orderBy
  // produces numeric sorted order.  If the field were a string, the order would be "lexicographic" which
  // means "12" sorts before "3". It's a nuisance, however, since later we will be inputting productNumber as
  // a string in order to demonstrate validation t in productNumberIsNumeric and also using it
  // to demonstrate dynamic routes, which also demand the use of strings. The .toString and parseInt
  // javascript functions are used to perform conversions where required.
  const productsSnapshot = await getDocs(productsQuery);

  let currentProducts = [];

  productsSnapshot.forEach((product) => {
    currentProducts.push({ productNumber: product.data().productNumber });
  });

  return { products: currentProducts }
}

export const actions = {
  default: async (event) => {

    const formData = await event.request.formData();
    // retrieve the newProductNumber string from the form and convert it to a number
    const productNumberAsText = formData.get('newProductNumber');
    const newProductNumber = parseInt(productNumberAsText, 10)

    const productsDocData = { productNumber: newProductNumber }
    const productsCollRef = collection(db, "products");
    const productsDocRef = doc(productsCollRef);
    await setDoc(productsDocRef, productsDocData);

    return ({updateSucceeded: true})

  }
};
Enter fullscreen mode Exit fullscreen mode

If you copy this code, remember to reset the firestoreConfig data again. If you have problems getting it to work, remember also the advice in the previous post about debugging client-side code and look at the "Postscript sections" below for advice about fixing server-side problems. Good luck!

Note that the FireStore API imports and db configuration statements in +page.server.js have been given "file scope" by moving them out of the functions they serve and relocating them at the top of the file's <script> section. In a more complex project, it would be common practice to configure db in a separate lib folder to enable it to be shared more widely as an import.

You'll also note that the new code drops the resetting of the popupVisible field that previously appeared in the old +page.svelte file's on:click function. Svelte's default action on form submission is to reload the page. Consequently, when the form has been processed, popupVisible is re-initialised as false, the "Currently-registered products:" array is refreshed from the updated Firestore products collection and the popup disappears. As a bonus, the new Product Number will be inserted in the correct sorted order, courtesy of the orderBy("productNumber", "asc") qualifier in the products getDocs.

Caveat: code like this will only work while your Firestore database is public. When you add rules to restrict a collection's access to authorised users (ie users who have "logged in"), the Firestore templates you've been using here will fail. Posts in the next section of this series will explain why and Post 3.4 in particular will explain how you can fall back to code based purely on client-side +page.svelte files. However, since this will be at the expense of security and performance, I hope you'll fight your way through these "minor irritations" and continue developing client-server code. For the present, make sure that your database rules look something like:

    match /products/{document=**} {
      // Allow both read and write access
      allow read, write: if true;
    }
Enter fullscreen mode Exit fullscreen mode

4. Summary

I imagine that this post will have strained your patience to the limit. If you've reached this point with a working webapp and your sanity still intact, give yourself a gold star - you've covered most of the core Svelte topics and got a handle on the essential skills!

That said, there's still a lot to learn. For example, this post has ducked out of describing basic error handling, and form validation arrangements. Beyond that, I also want to introduce you to Svelte routes(ie pages), layouts (page headers and trailers), components, and the tricky issues surrounding security (login design and database rules). Finally, there's the interesting question of how you deploy your webapp for live operation on the web. I hope you'll read on!

Postcript: When things go wrong

The introduction of "server-side" processing in your webapp has fast-tracked you straight into the senior developer league. This is because the "Chrome Inspector" techniques that I hope you've enjoyed using on your +page.svelte files don't work on +page.server.js files. But all is not lost. Here's an introduction to the techniques that a senior developer would use:

Postscript (a): Debugging Server side code in the Terminal Session

Although the VSCode editor will do its best to help you produce sound code, some basic errors will only become apparent when the SvelteKit server tries to run your webapp. At this point, your screen may display a "500 - Internal Error" message (if it displays anything at all!). The browser can't help you much here because a +page.server.js file is essentially invisible here. While the Source tab's Page/localhost storage hierarchy continues to show your +page.svelte file, it has nothing to say about +page.server.js.

But the Inspector knows that an error has occurred and can sometimes give you useful a clue as to its cause. The right-hand end of the menu bar will show a red icon with a cross in the middle. Click this and the Inspector's console will open and display summary error details. But if you need full details, you'll find these in the terminal session where you launched the server with npm run dev.

The problem here is that you'll likely feel the level of detail is probably rather too full. Every error is reported with a "call stack" that details the full sequence of server function calls that preceded the failure point Here's a short one precipitated by an incorrect field name declaration (I deliberately mistype const db= as const dba =

ReferenceError: db is not defined
    at load (C:\Users\mjoyc\Desktop\GitProjects\svelte-test\src\routes\+page.server.js:18:38)
    at Module.load_server_data (C:\Users\mjoyc\Desktop\GitProjects\svelte-test\node_modules\@sveltejs\kit\src\runtime\server\page\load_data.js:61:41)
    at C:\Users\mjoyc\Desktop\GitProjects\svelte-test\node_modules\@sveltejs\kit\src\runtime\server\page\index.js:140:19   
Enter fullscreen mode Exit fullscreen mode

A terminal window isn't a good place to view information like this. Sometimes you have to scroll up or down to find what you want, and information may be additionally obscured by output from other activities. Nevertheless, it's all you've got so you need to make the best of it.

But note that, in the example shown above, the cause of the error is signalled very clearly - the server has encountered a reference to a db variable in +page.server.js at line 18 column 38 and db hasn't been declared. That gives you everything you need, I think.

The terminal window can help you fix problems with your logic too. In the past, "debugging" was usually carried out by placing console.log messages messages at strategic points in the code and it's easy enough to use this approach here. A typical log message would be:

console.log("Arrived at X with a= " + a);
Enter fullscreen mode Exit fullscreen mode

A log message like this placed in a +page.server.js file will be displayed in the VSCode terminal window (by contrast, log statements in a client-side +page.svelte code will continue to be displayed in the browser's console window).

Postscript (b): Debugging Server side code in the VSCode debugger

Console.log messages are fine for fixing minor issues, but by now you expect to be able to use something with the sophistication of the browser "breakpoint" debugging tool. Fear not. You can now find this in the VSCode. editor. Here's what you do:

  1. Open the +page.server.js file you want to debug and set a breakpoint at a point you'd like to inspect. You do this by mousing over the source line you want to use as a breakpoint and clicking on the pale red dot that will appear at its left-hand side. The pale red dot now changes to bright red.
  2. In the VSCode command palette (shortcut "ctrl shift p") choose Debug: Attach to node process and get a list of projects in your workspace. Select the one you want to debug.
  3. Note that the terminal session that you've just launched is labelled as "Javascript Debug Terminal". Type 'npm run dev` into this terminal and note how the usual Vite output that appears includes an additional "Debugger attached" line. Note also how VSCode's activity bar has turned orange
  4. Now mouse over the http://localhost address displayed by Vite and launch the webapp with a "ctrl-click". Your webapp now opens in a popup window.
  5. You'll now find that the editor page for your +page.server.js file has become an active debugging session with execution halted on the first breakpoint. A panel at the top of the editor window displays the familiar "advance to next breakpoint" etc buttons, and mousing over field names will reveal tooltips that display their values. As before, if a debug session is halted on a variable assignment statement, you'll only see the result of this when you advance to the next statement.
  6. Terminate the debug session by mousing over the icon at the far end of the debug controls panel. This toggles between "disconnect" and "stop" actions when you press the "alt" key. Click this when it displays "disconnect" and the orange VSCode activity bar will turn blue again. If you want to resume debugging, "ctrl click" on the webapp URL in the debugging terminal window again (which will stay active until you explicitly "bin" it)

The screenshot below shows the +page.server.js file for this post halted on the return from the load() function. A "mouseover" on the products property for the return at this point displays the result of reading the Firestore products collection.

Graphic illustrating server-side debugging in VSCode

For full details of the facilities available in the VSCode debugger, see the documentation at VSCode debugging

đź’– đź’Ş đź™… đźš©
mjoycemilburn
MartinJ

Posted on November 26, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related