Implementing LiveViews in Node.js
Tags: node, webdev
Published: 2024-04-17 16:43:42

**Written by [Emmanuel John](https://blog.logrocket.com/author/emmanueljohn/)✏️**

LiveViewJS is an open source, full-stack framework for building LiveView-based, full-stack web applications in Node.js and Deno with ease. 

LiveViewJS leverages the power of WebSockets and server-side rendering to enable real-time updates and seamless user experiences like React, Vue, and more, but with far less code and complexity and far more speed and efficiency. 

In this article, we'll compare the LiveView model to SPAs, demonstrate how to implement LiveViews in Node.js, and discuss use cases for LiveViews in frontend applications.

## Prerequisites

To follow along with this tutorial, you should have the following:

*   Node.js version 18.x or above
*   Familiarity with TypeScript

## Types of web applications

Decades ago, we had fewer solutions for building web applications. But today, there are many solutions and we can categorize them as follows:

*   Traditional server-side rendered app
*   Traditional SPA
*   Universal SSR app
*   Static-generated app

Now, we also have the LiveView paradigm as the most recent solution for building full-stack web applications.

### Traditional single-page apps (SPAs)

SPAs are client-side rendered (CSR) web applications that use JavaScript to render contents in the browser without requiring a browser reload for every new page. So, rather than receiving the contents rendered to the HTML document, you receive barebones HTML from the server, and JavaScript will be used to load the content in the browser. 

The advantages of SPAs are rich user experience and easier development and deployment. Their downsides include slow initial load speed and poor performance on the search engine.

### Understanding LiveView

In the LiveView model, the server renders an HTML page when a user makes the initial HTTP request, and then a persistent WebSocket connection connects the page to the server, after which user-initiated events such as clicks, key events, form input, etc., are sent to the server in very small packets via the WebSocket. 

The moment the server receives the user-initiated events, it runs the business logic for that LiveView, calculates the newly rendered HTML, and then sends only the diffs to the client, which automatically updates the page with the diffs.

## LiveView vs. SPAs

If you've used any of these SPA frameworks (React, Vue, and Svelte) to build production-ready applications, you'll notice that you need a completely different backend to handle business logic and data persistence — typically a REST or GRAPHQL API — while you handle the state management and rendering on the client. 

While this is great, you'll need to write two code bases for both the frontend and backend and manage the communication between these two codebases. But LiveViewJS offers a single codebase that handles both the frontend and backend while enabling the same rich user experiences that SPAs enable and allows you to build real-time interactive applications at ease without integrating any third-party library. 

LiveViewJS greatly simplifies the full-stack web application development process, reduces the number of moving parts, and increases development speed. 

As you follow along with this tutorial, you'll see how easy it is to build rich, interactive, and responsive user experiences with LiveViewJS and gain a better understanding of how much of an improvement it is.

## Exploring LiveViews in Node.js

LiveViewJS is highly compatible with Node.js and its ecosystem, offering developers a powerful and flexible platform for building real-time web applications. Let's explore LiveViewjs APIs for more information.

### LiveViewSocket API

The `LiveViewSocket` API is one of the most important APIs in LiveView. This API consists of the following functions:

*   `socket.assign`
*   `socket.context`
*   `socket.pushEvent`
*   Server events

The `socket.assign` method and `socket.context` property are used to modify and read the state of the LiveView, respectively. 

The following code snippet modifies the context (current state) of the `LiveView`:

```javascript
socket.assign({ foo: "bar" });
```

This reads the context (current state) of the `LiveView`:

```javascript
socket.context.foo
```

The `socket.pushEvent` method enables the server to “push” data to the client and update the URL of the LiveView:

```javascript
socket.pushEvent({
  type: "my-event",
  foo: "bar",
});
```

Now, the client can listen to this event in two ways. 

Using `window.addEventListener`:

```javascript
window.addEventListener("phx:my-event", (event) => {
  console.log(event.detail.foo);
});
```

Using a client hook:

```javascript
this.handleEvent("my-event", (event) => {
  console.log(event.foo); 
});
```

### LiveViewSocket API: Server events

In a situation where a LiveView may need to wait for a long database query or search service to complete before rendering the results or sending updates based on a webhook or action from another user, server events are necessary. These server events help manage asynchronous processes. 

`socket.sendInfo` enables a LiveView to send messages:

```javascript
socket.sendInfo({ type: "run_search", query: event.query })
```

`socket.subscribe` enables a LiveView to subscribe to a topic using pub/sub. This is useful for cases where a LiveView needs to receive updates from another process or user:

```javascript
socket.subscribe("branches")
```

### User events

There are four main types of user events that a LiveView can listen and respond to:

*   Click events
*   Form events
*   Key events
*   Focus events

To listen for user events, there are a set of attributes that you need to add to the HTML elements in your LiveView render method.

### Value bindings

When you need to send additional data with an event binding, you can use a value binding, which looks something like `phx-value-[NAME]` where [`NAME`] is replaced by the key of the value you want to pass. 

For example, let's say you want to send the `mark_complete` event to the server along with an `id` value (e.g., `{id: "myId"}`) when the user clicks on the `Complete` button. To do this, you’d do the following:

```html
<button phx-click="mark_complete" phx-value-id="myId">Complete</button>
```

This example would send the following event to the server:

```javascript
{
  type: "mark_complete",
  id: "myId"
}
```

## Implementing LiveViews in Node.js

In this section, we'll build a full-stack Node.js application using LiveViewJS. The application will be a bank management application where a bank can set up multiple branches in different locations for its operations, update each branch's details, disable branches, and delete branches. 

Here is what the final application will look like: ![Final Bank Application](https://blog.logrocket.com/wp-content/uploads/2024/04/final-bank-application.png)

### Creating a new project

LiveViewJS has a project generation tool that will set up the project structure and install the required dependencies for either Node or Deno. 

First, run the following command:

```javascript
npx @liveviewjs/gen
```

You will be prompted to select the type of project you want to create and asked a few other questions. Then, voilà, you will have a new project that runs out of the box! 

Navigate to `src/server/liveTemplates.ts` and replace the default Tailwind script with your own CSS:

```typescript
<head>
  ...
  <script src="https://cdn.tailwindcss.com"></script>
</head>
```

We'll use [Tailwind CSS to style the application](https://blog.logrocket.com/best-tailwind-css-component-template-collections/).

### Creating the `inMemory` data store

To persist data within the application, we will set up an in-memory implementation of a database that works with changesets and pub/sub. 

Navigate to the `src` folder and create a `dataStore/inMemory.ts` file with the following:

```typescript
import { LiveViewChangeset, LiveViewChangesetFactory, newChangesetFactory, PubSub } from "liveviewjs";
import { SomeZodObject } from "zod";

type InMemoryChangesetDBOptions = {
  pubSub?: PubSub;
  pubSubTopic?: string;
};
```

Here, we’ll import all the necessary modules and types from the LiveViewJS and Zod libraries. 

Then, we’ll define a type `InMemoryChangesetDBOptions`, which is an object type with optional properties used to configure the in-memory database. 

Next, we'll define a `InMemoryChangesetDB` class, which serves as an in-memory database:

```typescript
export class InMemoryChangesetDB<T> {
  #store: Record<string, T> = {};
  #changeset: LiveViewChangesetFactory<T>;
  #pubSub?: PubSub;
  #pubSubTopic?: string;
}
```

We'll start by defining the following private properties:

*   **`#store`**: This property holds the data in a key-value pair format, where the key is a string representing the unique identifier of each data item and the value is of type `T`
*   **`#changeset`**: This property holds the factory function for creating changesets. It's used to create changesets for validating and modifying data
*   **`#pubSub`and`#pubSubTopicB`: These properties are optional and are used for publishing and subscribing to events. They are used for pub/sub communication to broadcast changes to subscribers

Next, we'll define the class constructor, which takes a schema of type `SomeZodObject` and an optional options object of type `InMemoryChangesetDBOptions`:

```typescript
export class InMemoryChangesetDB<T> {
  ...
  constructor(schema: SomeZodObject, options?: InMemoryChangesetDBOptions) {
    this.#changeset = newChangesetFactory(schema);
    this.#pubSub = options?.pubSub;
    this.#pubSubTopic = options?.pubSubTopic;
  }
}
```

The constructor initializes the `#changeset` property with a new changeset factory created from the provided schema. It also assigns the `#pubSub` and `#pubSubTopic` properties based on the provided options. 

Next, we'll define the `changeset` method, which creates and returns a new changeset:

```typescript
export class InMemoryChangesetDB<T> {
  ...
  changeset(existing?: Partial<T>, newAttrs?: Partial<T>, action?: string): LiveViewChangeset<T> {
    return this.#changeset(existing ?? {}, newAttrs ?? {}, action);
  }
}
```

Next, we'll create the data access methods to retrieve data from our data store:

```typescript
export class InMemoryChangesetDB<T> {
  ...  
  list(): T[] {
    return Object.values(this.#store);
  }

  get(id: string): T | undefined {
    return this.#store[id];
  }
}
```

The `list` method returns an array containing all the values stored in the database while the `get` method retrieves the value corresponding to the provided ID from the database. 

Next, create a `validate` method. This method creates a changeset with the provided data for validation purposes:

```typescript
export class InMemoryChangesetDB<T> {
  ...
  validate(data: Partial<T>): LiveViewChangeset<T> {
    return this.changeset({}, data, "validate");
  }
}
```

Now, we'll define methods for CRUD operations:

```typescript
export class InMemoryChangesetDB<T> {
  ...
  create(data: Partial<T>): LiveViewChangeset<T> {
    const result = this.#changeset({}, data, "create");
    if (result.valid) {
      const newObj = result.data as T;
      // assume there will be an id field
      this.#store[(newObj as any).id] = newObj;
      this.broadcast("created", newObj);
    }
    return result;
  }
}
```

The method above creates a new data item in the database. It first creates a changeset, checks if it's valid, adds the new data to the store, and broadcasts a `created` event using pub/sub. 

The following method updates an existing data item in the database:

```typescript
export class InMemoryChangesetDB<T> {
  ...
  update(current: T, data: Partial<T>): LiveViewChangeset<T> {
    const result = this.#changeset(current, data, "update");
    if (result.valid) {
      const newObj = result.data as T;
      this.#store[(newObj as any).id] = newObj;
      this.broadcast("updated", newObj);
    }
    return result;
  }
}
```

It creates a changeset, checks if it's valid, updates the data in the store, and broadcasts an `updated` event using pub/sub. 

Finally, this method deletes a data item from the database based on its `id`:

```typescript
export class InMemoryChangesetDB<T> {
  ... 
  delete(id: string): boolean {
    const data = this.#store[id];
    const deleted = data !== undefined;
    if (deleted) {
      delete this.#store[id];
      this.broadcast("deleted", data);
    }
    return deleted;
  }
}
```

It removes the item from the store and broadcasts a `deleted` event using pub/sub. 

The private `broadcast` method is responsible for broadcasting events using pub/sub:

```typescript
export class InMemoryChangesetDB<T> {
  ...
  private async broadcast(type: string, data: T) {
    if (this.#pubSub && this.#pubSubTopic) {
      await this.#pubSub.broadcast(this.#pubSubTopic, { type, data });
    }
  }
}
```

It checks if the pub/sub and the topic are defined, then broadcasts the provided `type` and `data` to subscribers.

### Building the CRUD features of the application

Create a new `src/server/liveview/bank.ts` file. This is where all application logic will live. 

We'll start by importing specific functions and objects from the Nano ID and Zod libraries:

```javascript
import {
  createLiveView,
  error_tag,
  form_for,
  html,
  LiveViewChangeset,
  newChangesetFactory,
  SingleProcessPubSub,
  submit,
  text_input,
} from "liveviewjs";
import { nanoid } from "nanoid";
import { z } from "zod";
```

### Schema setup

Next, we'll set up a Zod schema for a branch object, ensuring that any data conforming to this schema satisfies certain validation criteria:

```typescript
const BranchSchema = z.object({
  id: z.string().default(nanoid),
  name: z.string().min(2).max(100),
  manager: z.string().min(4).max(100),
  address: z.string().min(4).max(100),
  contact: z.string().min(4).max(100),
  status: z.boolean().default(false),
});
```

Next, we'll infer the `Branch` type from the `BranchSchema`:

```typescript
type Branch = z.infer<typeof BranchSchema>;
```

Then, add the following to create the branch `LiveViewChangesetFactory`:

```typescript
const branchCSF = newChangesetFactory<Branch>(BranchSchema);
```

`branchCSF` creates a factory for producing `LiveViewChangeset` instances tailored to the `Branch` type. 

Add the following to create an in-memory data store for `Branch` types:

```typescript
const branchesDB: Record<string, Branch> = {};
```

Add the following to create a pub/sub for publishing changes:

```typescript
const pubSub = new SingleProcessPubSub();
```

`pubSub` allows components of the application to subscribe to changes made to branches and publish updates when branches are modified. 

Declare the variable `editBranchId` outside the scope of the LiveView component to keep track of the ID of the branch currently being edited:

```javascript
let editBranchId = "";
```

Next, define the `branchesLiveView` component:

```typescript
export const branchesLiveView = createLiveView<
  // Define the Context of the LiveView
  {
    branches: Branch[];
    changeset: LiveViewChangeset<Branch>;
    editBranchId: string | null;
  },
  // Define events that are initiated by the end-user
  | { type: "save"; name: string; manager: string }
  | { type: "validate"; name: string; manager: string, address: string, contact: string }
  | { type: "toggle-status"; id: string }
  | { type: "edit"; id: string }
  | { type: "update"; id: string }
  | { type: "delete"; id: string }
>({
  mount: (socket) => {  },
  handleEvent: (event, socket) => {  },
  handleInfo: (info, socket) => {  },
  render: (context, meta) => {  }
})
```

This declares a LiveView component named `branchesLiveView` using the `createLiveView` function. It defines the context of the LiveView and events that are initiated by the end user. 

Update the mount method with the following:

```javascript
mount: (socket) => {
  if (socket.connected) {
    socket.subscribe("branches");
  }
  socket.assign({
    branches: Object.values(branchesDB),
    changeset: branchCSF({}, {}),
    editBranchId: null,
  });
}
```

The `mount` function is called when the LiveView is mounted onto the DOM. Here, we ensure that LiveView is subscribed to the `branches` channel if the socket connection is established, then initialize the LiveView's context with initial values, including the current list of branches, an empty changeset, and a null value for the `editBranchId` property. 

Update the `handleInfo` method with the following:

```javascript
handleInfo: (info, socket) => {
  if (info.type === "updated") {
    socket.assign({
      branches: Object.values(branchesDB),
    });
  }
}
```

`handleInfo` handles info messages received by the LiveView component, specifically those with the type `updated`. When such a message is received, it updates the LiveView context with the most recent list of branches, ensuring that the UI reflects the latest changes.

### Form validation

Form validation is an essential part of application development as it contributes to enhancing the security of the application. 

Update the `handleEvent` method with the following:

```javascript
handleEvent: (event, socket) => {
  switch (event.type) {
    case "validate":
      socket.assign({
        changeset: branchCSF({}, event, "validate"),
      });
      break;
  }
},
```

The `handleEvent` method is called when an event is triggered within the LiveView component. It processes different types of events and performs corresponding actions based on the event type. The validate case validates the form data. 

Next, update the `render` method as follows:

```javascript
render: (context, meta) => {
    const { changeset, branches, editBranchId } = context;
    const { csrfToken } = meta;
    return html`
      <h1 class="text-green-400 text-3xl mb-6 text-center">Cosmos Bank</h1>

      <div class="flex w-full justify-center">
        <div id="branchForm" class="bg-slate-100 w-[25rem] mb-8 rounded-xl p-8 bg-white">
          ${form_for<Branch>("#", csrfToken, {
      phx_change: "validate"
    })}

          <div class="space-y-4">
            <div>
              ${text_input(changeset, "name", { placeholder: "Branch Name", className: "w-full h-8 p-2 bg-gray-100", autocomplete: "off", phx_debounce: 1000 })}
              ${error_tag(changeset, "name", { className: "text-red-500 text-sm" })}
            </div>
            <div>
              ${text_input(changeset, "manager", { placeholder: "Manager", className: "w-full h-8 p-2 bg-gray-100", autocomplete: "off", phx_debounce: 1000 })}
              ${error_tag(changeset, "manager", { className: "text-red-500 text-sm" })}
            </div>
            <div>
              ${text_input(changeset, "address", { placeholder: "Address", className: "w-full h-8 p-2 bg-gray-100", autocomplete: "off", phx_debounce: 1000 })}
              ${error_tag(changeset, "address", { className: "text-red-500 text-sm" })}
            </div>
            <div>
              ${text_input(changeset, "contact", { placeholder: "Contact", className: "w-full h-8 p-2 bg-gray-100", autocomplete: "off", phx_debounce: 1000 })}
              ${error_tag(changeset, "contact", { className: "text-red-500 text-sm" })}
            </div>
          </div>
          <button class="flex justify-center bg-blue-700 mt-8 p-2 text-white w-full rounded-md">
            submit
          </button>
          </form>
        </div>
      </div>
    `;
  },
```

The `render` function takes two parameters: `context` and `meta` and returns HTML content using template literals (`html`). `context` contains the current state of the LiveView, while `meta` contains metadata such as `csrfToken`, which is used for security purposes. 

The `form_for` helper function dynamically generates a form, while the `text_input` helper function generates the input fields. The input fields are bound to the `changeset` object, allowing for real-time validation and updating of form data. The `error_tag` helper function displays error messages for each input field if validation fails. The `phx_change: "validate"` and `phx_debounce: 1000` trigger the validate case one second after the input change value changes. 

If you followed up to this point of the tutorial, you should have the following result: ![Input Field Error](https://blog.logrocket.com/wp-content/uploads/2024/04/input-field-error.png)

### Implementing the `create` feature

To add a new branch to our data store, we’ll implement the `create` feature. 

First, add the `save` case to the `switch`:

```javascript
handleEvent: (event, socket) => {
  switch (event.type) {
    ...
    case "save":
      // attempt to create the branch from the form data
      const saveChangeset = branchCSF({}, event, "save");
      let changeset = saveChangeset;
      if (saveChangeset.valid) {
        // save the branch to the in memory data store
        const newBranch = saveChangeset.data as Branch;
        branchesDB[newBranch.id] = newBranch;
        // since branch was saved, reset the changeset to empty
        changeset = branchCSF({}, {});
      }
      // update context
      socket.assign({
        branches: Object.values(branchesDB),
        changeset,
      });
      pubSub.broadcast("branches", { type: "updated" });
      break;
  }
}
```

The `save` case executes when the event type is `save` and handles the `save` event by validating form data, saving a new branch to the data store if the data is valid, resetting the changeset to empty, and updating the LiveView context with the changes. Finally, it broadcasts an update event to notify other components about the changes. 

Now, update the form submission button as follows:

```html
<div class="flex justify-center bg-blue-700 mt-8 p-2 text-white w-full rounded-md">
  ${submit("Add Branch", { phx_disable_with: "Saving..." })}
</div>
```

Then add the `phx_submit: "save"` to the `form_for` helper function as follows:

```javascript
${form_for<Branch>("#", csrfToken, {
  phx_submit: "save",
  phx_change: "validate",
})}
```

The modified form component should look like the following:

```html
return html`
  <h1 class="text-green-400 text-3xl mb-6 text-center">Cosmos Bank</h1>

  <div class="flex w-full justify-center">
    <div id="branchForm" class="bg-slate-100 w-[25rem] mb-8 rounded-xl p-8 bg-white">
      ${form_for<Branch>("#", csrfToken, {
  phx_submit: "save",
  phx_change: "validate",
})}

      <div class="space-y-4">
        <div>
          ${text_input(changeset, "name", { placeholder: "Branch Name", className: "w-full h-8 p-2 bg-gray-100", autocomplete: "off", phx_debounce: 1000 })}
          ${error_tag(changeset, "name", { className: "text-red-500 text-sm" })}
        </div>
        <div>
          ${text_input(changeset, "manager", { placeholder: "Manager", className: "w-full h-8 p-2 bg-gray-100", autocomplete: "off", phx_debounce: 1000 })}
          ${error_tag(changeset, "manager", { className: "text-red-500 text-sm" })}
        </div>
        <div>
          ${text_input(changeset, "address", { placeholder: "Address", className: "w-full h-8 p-2 bg-gray-100", autocomplete: "off", phx_debounce: 1000 })}
          ${error_tag(changeset, "address", { className: "text-red-500 text-sm" })}
        </div>
        <div>
          ${text_input(changeset, "contact", { placeholder: "Contact", className: "w-full h-8 p-2 bg-gray-100", autocomplete: "off", phx_debounce: 1000 })}
          ${error_tag(changeset, "contact", { className: "text-red-500 text-sm" })}
        </div>
      </div>
      <div class="flex justify-center bg-blue-700 mt-8 p-2 text-white w-full rounded-md">
        ${submit("Add Branch", { phx_disable_with: "Saving..." })}
      </div>
      </form>
    </div>
  </div> 
`;
```

### Implementing the `read` feature

To view the list of created branches, add the list component just before the closing HTML backtick as follows:

```html
return html`
  //Form
  ...
  <div id="branches" class="flex flex-wrap space-x-4 items-center justify-center">
    ${branches.map((branch) => renderBranch(branch, csrfToken, editBranchId))}
  </div>
`;
```

The `branches` array from `context` is mapped over, and each branch is rendered using the `renderBranch` function. 

Then, create the `renderBranch` component as follows:

```html
function renderBranch(b: Branch, csrfToken: any, editBranchId: string | null) {
  return html`
    <figure id="${b.id}" class="flex bg-slate-100 w-[30rem] mt-4 rounded-xl p-8 md:p-0 bg-white">
        <img class="w-24 h-24 md:w-48 md:h-auto  md:rounded-l-lg" src="https://media.wired.com/photos/59269cd37034dc5f91bec0f1/master/pass/GoogleMapTA.jpg" alt="" width="384" height="512">
        <div class="pt-6 md:p-8 text-center md:text-left">
          <div class="space-y-1">
            <p class="text-base font-normal">
              Branch name: ${b.name}
            </p>
            <p class="text-base font-normal">
              Address: ${b.address}
            </p>
            <p class="text-base font-normal">
              Contact: ${b.contact}
            </p>
            <p class="text-base font-normal">
              Total Staff: 24
            </p>
          </div>
          <div class="flex space-x-2 items-center mt-8">
            <img class="w-10 h-10 rounded-full" src="https://media.wired.com/photos/59269cd37034dc5f91bec0f1/master/pass/GoogleMapTA.jpg" alt=""/>
            <figcaption class="font-medium">
              <div class="text-sky-500 dark:text-sky-400">
              ${b.manager}
              </div>
              <div class="text-slate-700 dark:text-slate-500">
                Branch Manager
              </div>
            </figcaption>
          </div>
        </div>
      </figure>
  `;
}
```

The `renderBranch` function dynamically generates HTML markup to display information about a branch, including its name, status, and manager, as well as providing options for editing. 

If you have followed up to this point, you should have the following result: ![Bank Application With Branch Information](https://blog.logrocket.com/wp-content/uploads/2024/04/bank-appliction-with-branch-information.png)

### Implementing the `update` feature

To update the information for an existing branch in our web app, we’ll implement a status update using the `update` feature. 

First, we'll implement the status update, then we’ll implement an update for the entire branch. 

Add the `toggle-status` case to the switch:

```javascript
handleEvent: (event, socket) => {
  switch (event.type) {
    ...
    case "toggle-status":
      const branch = branchesDB[event.id];
      if (branch) {
        branch.status = !branch.status;
        branchesDB[branch.id] = branch;
        socket.assign({
          branches: Object.values(branchesDB),
        });
        pubSub.broadcast("branches", { type: "updated" });
      }
      break;
  }
}
```

The `toggle-status` case executes when the event type is `toggle-status`, handles the `toggle-status` event by toggling the status of the branch specified in the event, updating the data store, LiveView context, and broadcasting the changes to other components. 

Add the `edit` case to the switch:

```javascript
handleEvent: (event, socket) => {
  switch (event.type) {
    ...
    case "edit":
      editBranchId = event.id;
      socket.assign({
        editBranchId: event.id,
      });
      break;
  }
}
```

The `edit` case executes when the user triggers an `edit` event. It updates the `editBranchId` variable and the LiveView context with the ID of the branch being edited. This allows the UI to reflect that the user is currently editing a specific branch. 

Add the `update` case to the switch:

```javascript
handleEvent: (event, socket) => {
  switch (event.type) {
    ...
    case "update":
      const editChangeset = branchCSF(branchesDB[editBranchId], event, "save");
      if (editChangeset.valid) {
        const editedBranch = editChangeset.data as Branch;
        branchesDB[editedBranch.id] = editedBranch;

        socket.assign({
          branches: Object.values(branchesDB),
          changeset: branchCSF({}, {}),
          editBranchId: null,
        });

        pubSub.broadcast("branches", { type: "updated" });
      }
      break;
  }
}
```

The `update` case executes when the user triggers an `update` event. It edits an existing branch with the provided data, updating the branch in the in-memory data store and the LiveView context, and broadcasting the changes to other components if the update is successful. 

Next, update the `renderBranch` component as follows:

```javascript
function renderBranch(b: Branch, csrfToken: any, editBranchId: string | null) {
  return html`
    <figure id="${b.id}" class="flex bg-slate-100 w-[30rem] mt-4 rounded-xl p-8 md:p-0 bg-white">
        <img class="w-24 h-24 md:w-48 md:h-auto  md:rounded-l-lg" src="https://media.wired.com/photos/59269cd37034dc5f91bec0f1/master/pass/GoogleMapTA.jpg" alt="" width="384" height="512">
        <div class="pt-6 md:p-8 text-center md:text-left">
          <div class="space-y-1">
            <p class="text-base font-normal">
              Branch name: ${b.name}
            </p>
            <p class="text-base font-normal">
              Address: ${b.address}
            </p>
            <p class="text-base font-normal">
              Contact: ${b.contact}
            </p>
            <p class="text-base font-normal">
              Total Staff: 24
            </p>
          </div>
          <button class="${b.status ? 'bg-red-700' : "bg-green-700"} px-4 py-1.5 rounded-md text-white" phx-click="toggle-status" phx-value-id="${b.id}" phx-disable-with="Updating...">
            ${b.status ? "Disabled" : "Activated"}
          </button>
          <div class="flex space-x-2 items-center mt-8">
            <img class="w-10 h-10 rounded-full" src="https://media.wired.com/photos/59269cd37034dc5f91bec0f1/master/pass/GoogleMapTA.jpg" alt=""/>
            <figcaption class="font-medium">
              <div class="text-sky-500 dark:text-sky-400">
              ${b.manager}
              </div>
              <div class="text-slate-700 dark:text-slate-500">
                Branch Manager
              </div>
            </figcaption>
          </div>
          ${editBranchId === b.id ? editForm(b, csrfToken) : editButton(b.id)}
        </div>
      </figure>
  `;
}
```

Next, create the `editButton` components as follows:

```javascript
function editButton(id: string) {
  return html`
    <button class="bg-blue-700 text-white text-sm py-2 px-4 rounded-md" phx-click="edit" phx-value-id="${id}">Edit</button>
  `;
}
```

The `editButton` function generates HTML markup for rendering buttons related to editing a branch, with dynamic IDs and event-handling attributes. 

Next, we’ll create the `editForm` components as follows. The `editForm` function generates HTML markup for rendering an editable form to update branch information, with input fields for each attribute and a submit button to trigger the update action:

```javascript
function editForm(branch: Branch, csrfToken: any) {
  return html`
    ${form_for<Branch>("#", csrfToken, {
    phx_submit: "update",
  })}
      <div class="space-y-2">
        <div class="field">
          ${text_input(branchCSF({}, branch), "name", { placeholder: "name", autocomplete: "off", className: "w-full border h-8 p-2 bg-gray-100" })}
        </div>
        <div class="field">
          ${text_input(branchCSF({}, branch), "manager", { placeholder: "manager", autocomplete: "off", className: "w-full border h-8 p-2 bg-gray-100" })}
        </div>
        <div class="field">
          ${text_input(branchCSF({}, branch), "address", { placeholder: "address", autocomplete: "off", className: "w-full border h-8 p-2 bg-gray-100" })}
        </div>
        <div class="field">
          ${text_input(branchCSF({}, branch), "contact", { placeholder: "contact", autocomplete: "off", className: "w-full border h-8 p-2 bg-gray-100" })}
        </div>
        <div class="flex justify-center bg-blue-700 mt-8 p-2 text-white w-full rounded-md">
          ${submit("Update Branch", { phx_disable_with: "Updating...", phx_value_id: branch.id })}
        </div>
      </div>
    </form>
  `;
}
```

Users can now toggle between **disabled** and **activated** for the status of each branch as well as update the branch details: ![Displaying Disabled Branch Information](https://blog.logrocket.com/wp-content/uploads/2024/04/displaying-disabled-branch-information.png)

### Implementing the `delete` feature

To delete branches from our web application, we’ll add the `delete` case to the `switch`:

```javascript
handleEvent: (event, socket) => {
  switch (event.type) {
    ...
    case "delete":
      delete branchesDB[event.id];
      socket.assign({
        branches: Object.values(branchesDB),
      });
      pubSub.broadcast("branches", { type: "updated" });
      break;
  }
}
```

The `delete` case handles the `delete` event by removing the specified branch from the data store, updating the LiveView context with the updated list of branches, and broadcasting the changes to other components. 

Now, add the delete button to the `editButton` component as follows:

```javascript
function editButton(id: string) {
  return html`
    <button class="bg-blue-700 text-white text-sm py-2 px-4 rounded-md" phx-click="edit" phx-value-id="${id}">Edit</button>
    <button class="bg-red-700 text-white text-sm py-2 px-4 rounded-md" phx-click="delete" phx-value-id="${id}" phx-disable-with="Deleting...">Delete</button>
  `;
}
```

Users should be able to delete a branch by clicking on the **delete** button: ![Delete Button To Delete A Deactivated Branch](https://blog.logrocket.com/wp-content/uploads/2024/04/delete-button-to-delete-deactivated-branch.png) To test the final version of our demo application, run the following command:

```javascript
npm run dev
```

To test the real-time interactivity in our demo web application, open `http://localhost:4001` with your browser and also do the same in incognito mode. Notice how the branches are created, updated, and deleted in real time. 

You just built a real-time interactive web application with fewer lines of code compared to using any SPA or frontend framework. How easy was that?

## Conclusion

In this tutorial, we took a look at some comparisons between the LiveView model and SPAs, demonstrated how to implement LiveViews in Node.js, and successfully built a full-stack bank management application with support for real-time interactivity. There are so many ways this can be improved, and I can’t wait to see what you build next with LiveViewJS. 

You can find the [complete source code on GitHub](https://github.com/emmanuelhashy/liveviewjs-bank-management-app).

---

##200s only✓ Monitor failed and slow network requests in production

Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third-party services are successful, [try LogRocket](https://lp.logrocket.com/blg/node-signup).

[![LogRocket Signup](https://blog.logrocket.com/wp-content/uploads/2019/12/network-request-filter-2-1.png)](https://lp.logrocket.com/blg/node-signup)

[LogRocket](https://lp.logrocket.com/blg/node-signup) is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.

LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. [Start monitoring for free](https://lp.logrocket.com/blg/node-signup).