left-icon

Svelte Succinctly®
by Ed Freitas

Previous
Chapter

of
A
A
A

CHAPTER 6

Favorites UI and Books Component

Favorites UI and Books Component


Quick intro

We will focus on the Favorites page's user interface, which is only accessible once a user has signed in.

The Favorites page resides within the src/routes/favorites folder within the project’s structure and has two parts, __layout.svelte and index.svelte.

To get a visual understanding of how the __layout.svelte and index.svelte files make up the Favorites page, let’s look at the following figure.

Favorites Page (__layout.svelte and index.svelte)

Figure 6-a: Favorites Page (__layout.svelte and index.svelte)

We can see that the Favorites page has a navigation bar and that markup is found within the __layout.svelte file (highlighted in purple).

The Favorites page also displays a list of favorite books, and that markup is found within the index.svelte file (highlighted in yellow).

As we’ll see later, most of the index.svelte references Books.svelte, which is the component that encapsulates the markup for displaying the list of books.

Let’s begin by exploring __layout.svelte.

Favorites page (__layout.svelte)

The following listing shows the finished code for the __layout.svelte file found within the src/routes/favorites folder (not to be confused with the __layout.svelte file located under the root of the src/routes folder).

Code Listing 6-a: The finished __layout.svelte File (src/routes/favorites folder)

<script>

    import { onAuthStateChanged, signOut } from "Firebase/auth"

    import { onMount } from "svelte"

    import { auth } from "../../firebase.js"

    import { getStores } from "$app/stores"

    import { goto } from "$app/navigation"

 

    let { session } = getStores()

 

    onMount(() => {

          onAuthStateChanged(

              auth,

              (user) => {

                  session.set({ user })

              },

              (error) => {

                  session.set({ user: null })

                  console.log(error)

              }

          );

    });

 

    const logOut = async () => {

        await signOut(auth)

        await goto('/')

    }

</script>

<div id="page-wrapper"

    class="page-wrapper with-navbar"

    data-sidebar-type="overlayed-sm-and-down">

    <nav class="navbar">

        <div class="navbar-content">

        </div>

        <a href="/"

            class="navbar-brand ml-10 ml-sm-20">

            <span class="d-none d-sm-flex">

                FavBooks

            </span>

        </a>

        <span class="navbar-text ml-5">

            {#if $session['user'] != null}

              <span class="ml-10 mt-5 badge text-monospace">

                  {$session['user'].email}'s favorites

              </span>

            {/if}

        </span>

        <div class="navbar-content ml-auto">

            <button

                class="btn btn-action mr-5"

                type="button"

                onclick="halfmoon.toggleDarkMode()">

                <i class="fa fa-moon-o"

                    aria-hidden="true">

                </i>

                <span

                    class="sr-only">

                    Toggle dark mode

                </span>

            </button>

            {#if $session['user'] != null}

            <a href={null}

                class="mr-5 btn btn-success"

                role="button"

                on:click={() => logOut()}

                >

                Sign out

            </a>

            {:else}

            <a href="/login"

                class="mr-5 btn btn-secondary"

                role="button">

                Sign in

            </a>

            {/if}

        </div>

    </nav>

    <slot />

</div>

Before reviewing the details of the code, let’s have a quick look at the following figure, to get a visual understanding of how __layout.svelte is structured.

Favorites Page (Focusing on __layout.svelte)

Figure 6-b: Favorites Page (Focusing on __layout.svelte)

Looking at the preceding image, we can see that the part highlighted in yellow corresponds to the page's title. When you click it, you’ll be redirected to the app’s main page.

The section in red corresponds to the user's email that is signed in. The section highlighted in green corresponds to the dark mode toggle button.

The section in purple corresponds to the Sign out button, and the section in blue corresponds to the slot tag where the list of favorite books is shown.

Now, let’s focus on the code. Let’s begin with the import statements.

import { onAuthStateChanged, signOut } from "Firebase/auth"

import { onMount } from "svelte"

import { auth } from "../../firebase.js"

import { getStores } from "$app/stores"

import { goto } from "$app/navigation"

From the Firebase/auth library, we import the onAuthStateChanged event and the signOut method.

From the Svelte core module (svelte), we import the onMount lifecycle event. Then, we import the auth object from the firebase.js utility file (which we’ll explore later).

We also import the getStores method from the app/stores module and the goto method from app/navigation.

By invoking the getStores method, we can get the current session information by destructuring the result. The session information is used for accessing user details.

let { session } = getStores()

When the component mounts, we subscribe to the Firebase onAuthStateChanged event, which we can use to assign the current user details to the active session.

onMount(() => {

  onAuthStateChanged(

    auth,

    (user) => {

      session.set({ user })

    },

    (error) => {

      session.set({ user: null })

      console.log(error)

    }

  );

});

This way, we always know which user is active and signed in. Finally, the logOut function executes when the Sign out button is clicked.

const logOut = async () => {

  await signOut(auth)

  await goto('/')

}

This function invokes the Firebase signOut method, which logs out the signed-in user, and then redirects the user to the application’s main page.

With regards to the markup, we find the following conditional rendering.

{#if $session['user'] != null}

  <span class="ml-10 mt-5 badge text-monospace">

    {$session['user'].email}'s favorites

  </span>

{/if}

For the currently signed-in user ($session['user'] != null), the user’s email address will be shown {$session['user'].email}.

The Sign out button will be rendered if the user has signed in ($session['user'] != null). Otherwise, the Sign in button is displayed (as a fail-safe option).

{#if $session['user'] != null}

  <a href={null}

   class="mr-5 btn btn-success"

   role="button"

   on:click={() => logOut()}

  >

   Sign out

  </a>

{:else}

  <a href="/login"

   class="mr-5 btn btn-secondary"

   role="button">

   Sign in

  </a>

{/if}

You might have noticed that it’s not in the __layout.svelte file that the redirection to login.svelte happens when there’s no signed-in user. This occurs in the load function of index.svelte, as we’ll see shortly.

Within __layout.svelte, we subscribe and retrieve the current session and user details, and do not take action based on the user status (signed-in or not).

Finally, we have the slot tag.

Favorites page (index.svelte)

The markup content of the index.svelte file will be inserted into the slot tag found within __layout.svelte.

Let’s have a look at the finished code of index.svelte found within the src/routes/favorites folder.

Code Listing 6-b: The Finished index.svelte File (src/routes/favorites folder)

<script context="module">

    import Books from "$lib/Books.svelte"

    import { auth,

        delFav } from "../../firebase.js"

    export const load = async () => {

          if (auth?.currentUser == null) {

              return {

                  status: 302,

                  redirect: "/login",

              }

          }

       

          return {

              status: 200

          }

    }

</script>

<script>

    import { navigating } from '$app/stores'

   

    const fav = "fav"

    const btnAction = async (event) => {

        await delFav(event.detail.uuid)

    }

</script>

{#if $navigating}

    <p>Fetching favorites...</p>

{:else}

    <Books books=favorites {fav}

        btnText="Remove"

        on:btnAction={btnAction} />

{/if}

The code within the <script context="module"> tag runs outside the component instance. First, we import what we need.

import Books from "$lib/Books.svelte"

import { auth, delFav } from "../../firebase.js"

In this case, we import the Books component from the Books.svelte file and import the auth object and delFav function from our firebase.js utility file.

Next, we find the load function, which checks whether the current user is authenticated, and if not, redirects the user to the Sign in page (login.svelte).

export const load = async () => {

  if (auth?.currentUser == null) {

    return {

      status: 302,

      redirect: "/login",

    }

  }

       

  return {

    status: 200

  }

}

If the current user is authenticated, then a status with a value of 200 is returned, and no redirect takes place, which means that the browser stays on the Favorites page.

Next, within the script tag that runs within the component instance, we import navigating from app/stores. We’ll use navigating to know whether the browser has fully loaded the page or not.

Then, because we are within the Favorites page, we declare fav and give it a non-empty value: const fav = "fav".

If we don’t do this, when we pass an empty value to the Books component, the Books component will assume that we are working with the static list of available books, not the list of favorite books (obtained from Firebase).

Note: Assigning the value “fav” to fav is critical for the Books component and the Favorites page to function correctly.

Next, we find the btnAction function, responsible for removing a specific book from the list of favorite books (stored in Firebase).

const btnAction = async (event) => {

  await delFav(event.detail.uuid)

}

Notice that this function receives the event parameter dispatched from the Books component, which contains the book's details to be removed from the list of favorite books.

The book's details to be removed are accessible via the detail property (event.detail), and uuid (event.detail.uuid) represents the book's unique identifier within Firebase.

The book's removal from Firebase is done by invoking the delFav function contained within the firebase.js utility file, which we’ll explore later.

Regarding the markup, all that happens is that we conditionally render the Books component (to show the list of favorite books) if the page has fully loaded (determined by checking the value of $navigating).

{#if $navigating}

    <p>Fetching favorites...</p>

{:else}

    <Books books=favorites {fav}

     btnText="Remove"

     on:btnAction={btnAction} />

{/if}

Notice that on this occasion, as part of the Books component parameters, we assign to the books property the value of favorites, and to the button’s text (btnText), the text Remove is assigned.

Notice how the function btnAction binds to btnAction dispatched from within the Books component.

Great! We are now ready to see where the magic happens by exploring the Books component.

Books.svelte

One fundamental part of the UI is required by both the application’s main page and the Favorites page, and that’s the Books component.

The Books.svelte file is located under the src/lib folder, and it encapsulates all the logic of the reusable Books component.

Perhaps the most remarkable feature of the component is its ability to display the list of available books obtained through the execution of src/routes/api/index.js and the list of favorite books from Firebase.

We already know how the Books component renders its elements. For example, the Books component displays the list of available books on the application’s main page (when a user is not signed in).

The Books Component (As Seen on the Main Page—No Signed-in User)

Figure 6-c: The Books Component (As Seen on the Main Page—No Signed-in User)

The following figure shows how the Books component displays the list of favorite books for a signed-in user.

The Books Component (As Seen on the Favorites Page—The User Signed In)

Figure 6-d: The Books Component (As Seen on the Favorites Page—The User Signed In)

The difference between both scenarios is the value passed to the books property and how the Books component reacts accordingly. To understand this better, let’s explore this component's finished code.

Code Listing 6-c: The Finished Books.svelte File (src/lib folder)

<script>

    import { createEventDispatcher,

        onMount } from "svelte"

    import { getStores } from "$app/stores"

    import { getFavs } from "../firebase.js"

    const dispatch = createEventDispatcher()

    let { session } = getStores()

    export let books = []

    export let fav = ""

    export let btnText

    onMount(async () => {

      if (fav !== "") {

        books = []

        books = await getFavs()

      }

    })

    const emitBtnAction = (book) => {

        dispatch("btnAction", book)

       

        if (fav !== "") {

            books = books.filter(item => item.title != book.title)

        }

    }

 </script>

{#if books?.length > 0}

    <div class="content-wrapper">

        <div class="container-fluid">

        <div class="content">

            <h2 class="content-title">

            Books

            </h2>

        </div>

        <div class="row row-eq-spacing">

            {#each books as book}

                <div class="col-6 col-lg-3">

                    <div class="mb-20 card">

                        <a target="_blank" href={book.url}>

                        <img src={book.cover}

                            class="img-fluid rounded"

                            alt="book cover" />

                        </a>

                        <div

                            class=  

                                "{fav === 'fav' ?

                                'alert-secondary ' :

                                'alert-primary '}

                                text-center alert"

                            role="alert">

                            {book.description}

                            {#if $session['user'] != null}

                                <a href={null}

                                    class=

                                    "{fav === 'fav' ?

                                    'btn-danger ' :

                                    'btn-primary '}

                                    mt-10 btn

                                    btn-block"

                                    role="button"

                                    on:click={() => emitBtnAction(book)}>

                                    {btnText}

                                </a>

                            {/if}

                        </div>

                    </div>

                </div>

            {/each}

        </div>

        </div>

    </div>

{:else}

    <div class="content-wrapper">

    <div class="row row-eq-spacing">

      <div class="col-sm"></div>

      <div class="col-sm">

        <div class="text-center alert alert-primary" role="alert">

            <h4 class="text-center alert-heading">

                No {fav !== "" ? 'favorites' : 'books'} found

            </h4>

            Click <a href="/" class="alert-link">here</a> to add one :).

        </div>

      </div>

      <div class="col-sm"></div>

    </div>

  </div>

{/if}

When the component instance runs, the code contained within the script tag executes. We begin, as usual, by importing what we need.

import { createEventDispatcher, onMount } from "svelte"

import { getStores } from "$app/stores"

import { getFavs } from "../firebase.js"

First, we import createEventDispatcher and the onMount lifecycle event from the Svelte core module (svelte).

Next, we import the getStores method from app/stores, which we’ll use to get the session and user details.

And finally, we import the getFavs function from our firebase.js utility file, which will be responsible for retrieving from Firebase the list of favorite books for the signed-in user.

Following that, we create an event dispatcher instance, which, as its name implies, will be used to emit an event to the parent components that will implement the Books component. The parent components are src/routes/index.svelte and src/routes/favorites/index.svelte.

const dispatch = createEventDispatcher()

Next, we invoke the getStores method to get the current session information.

let { session } = getStores()

After we declare the component properties in Svelte (as exported variables), this is how Svelte component properties are created:

export let books = []

export let fav = ""

export let btnText

Then, on the component’s onMount lifecycle event, if the value of fav is not an empty string (which means that we want to get the list of favorite books), the getFavs function is invoked, and the list of favorite books is retrieved from Firebase.

onMount(async () => {

  if (fav !== "") {

    books = []

    books = await getFavs()

 }})

Then, we have the emitBtnAction function, primarily responsible for dispatching the btnAction event to the parent components that implement the Books component.

const emitBtnAction = (book) => {

  dispatch("btnAction", book)

       

  if (fav !== "") {

    books = books.filter(item => item.title != book.title)

  }

}

That event dispatching is needed so that src/routes/index.svelte can implement the Add to favorites button functionality for each book, and src/routes/favorites/index.svelte can implement the Remove button functionality for each book.

Remember that the Add to favorites button functionality adds the book to Firebase as a favorite book, and the Remove button functionality deletes the book from Firebase.

Notice, however, that the actual deletion of a book from the books array (only when the Favorites page is displayed—fav !== "") is not delegated to a parent component, but done directly within the Books component.

That’s because the books array is not the responsibility of any parent component, but instead of the Books component. The Books component contains the local copy of the list of books on display.

Moving on to the markup, we find the following conditional rendering.

{#if books?.length > 0}

This means that the list of books (either the static list of available books or the list of favorite books obtained from Firebase) will only be rendered if the books array contains at least one book.

Otherwise ({:else}), a message is shown telling the user that there are no books to display, and how the user can add a book to the favorites list—redirecting the user to the application’s main page.

<div class="text-center alert alert-primary" role="alert">

  <h4 class="text-center alert-heading">

    No {fav !== "" ? 'favorites' : 'books'} found

  </h4>

  Click <a href="/" class="alert-link">here</a> to add one :).

</div>

Notice that this message and scenario only apply to the Favorites page because the static list of available books is prefilled and never empty, as we’ll see later.

Here comes the exciting part: how the books are rendered. This is possible thanks to Svelte’s iterator: {#each books as book}.

In essence, for every book contained within the books array, the book’s properties, including the book’s cover (book.cover), will be rendered.

Notice that only if a user has signed in ($session['user'] != null) will the Add to favorites button or the Remove button be shown, depending on the value of fav.

The value of fav also determines the button’s color by establishing the correct CSS class to apply to the button.

{#if $session['user'] != null}

  <a href={null}

   class=

   "{fav === 'fav' ?

   'btn-danger ' :

   'btn-primary '}

   mt-10 btn

   btn-block"

   role="button"

   on:click={() => emitBtnAction(book)}>

   {btnText}

  </a>

{/if}

When fav === 'fav', it means that the book is already part of the favorites list, and as such, the btn-danger CSS class makes the button red, making it a Remove button.

When fav !== 'fav', it means that the book is not part of the favorites list but instead part of the static list of available books, and as such, the btn-primary CSS class makes the button blue; making it an Add to favorites button.

When either the Add to favorites button or the Remove button is clicked, the emitBtnAction function is executed. Thus, the event is dispatched to the parent component to implement the respective functionality, with the currently selected book passed as a parameter.

Recap

Well done for following along until now! We finally have the application’s UI ready. Next, we’ll wrap up the application and book by exploring the code that makes the back-end part of the app work.

Scroll To Top
Disclaimer
DISCLAIMER: Web reader is currently in beta. Please report any issues through our support system. PDF and Kindle format files are also available for download.

Previous

Next



You are one step away from downloading ebooks from the Succinctly® series premier collection!
A confirmation has been sent to your email address. Please check and confirm your email subscription to complete the download.