DEV Community

Cover image for Create CRUD for managing webshop products
Dev By RayRay
Dev By RayRay

Posted on

Create CRUD for managing webshop products

How To Build A Serverless Webshop — part 3

In this part of the series we are going to explore how to build CRUD endpoints for our products.

We will make it possible to create new products, update existing products, and even delete them.

In the next part of the series, we will make sure that not everyone can add, update, or delete our products with authentication.

If you are ready to get your hands dirty with the FaunaDB API, please follow along.

Happy coding! 🚀


1. Product admin

To manage our products we need to have a product admin page.

ng generate component products/components/product-admin
Enter fullscreen mode Exit fullscreen mode

To access this page we need to create a route to access all the product data.

import { NgModule } from '@angular/core'
import { Routes, RouterModule } from '@angular/router'
import { ProductListComponent } from './products/components/product-list/product-list.component'
import { ProductItemComponent } from './products/components/product-item/product-item.component'
import { ProductAdminComponent } from './products/components/product-admin/product-admin.component'

const routes: Routes = [
    {
        path: '',
        component: ProductListComponent,
    },
    {
        path: 'product/:id',
        component: ProductItemComponent,
    },
    {
        path: 'admin',
        component: ProductAdminComponent,
    },
]

@NgModule({
    imports: [RouterModule.forRoot(routes)],
    exports: [RouterModule],
})
export class AppRoutingModule {}

Enter fullscreen mode Exit fullscreen mode

In the ​app.component.html​ we add a button to navigate to the admin page.

<div class="toolbar" role="banner">
    <h1 class="name">FaunaDB Webshop</h1>
    <nav>
        <button [routerLink]="['/']" mat-flat-button>Home</button>
        <button [routerLink]="['/admin']" mat-flat-button>Admin</button>
    </nav>
</div>

<div class="content" role="main">
    <router-outlet></router-outlet>
</div>
Enter fullscreen mode Exit fullscreen mode

2. Making Forms Easier

Making forms in Angular or any other web app is a time consuming job. To make it much easier to create and maintain the forms I will use NGX-Formly.

If you want to check out a more detailed tutorial about all the details in NGX-Formly, check my ​ first ​ and ​ second part here.

Adding Formly can be done via the Angular CLI. In this case I’ve added the Material plugin for Formly in the command below. You can replace ​material​ with ​bootstrap​ or
anything ​that they offer​.

ng add @ngx-formly/schematics --ui-theme=material
Enter fullscreen mode Exit fullscreen mode

Now the Angular CLI has added the Formly module to the ​app.module.ts​. But we also need to add the Material modules for using the Material input components in our forms.

import { BrowserModule } from '@angular/platform-browser'
import { NgModule } from '@angular/core'

import { AppRoutingModule } from './app-routing.module'
import { AppComponent } from './app.component'
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'

import { MatButtonModule } from '@angular/material/button'

import { HttpClientModule } from '@angular/common/http'
import { ProductListComponent } from './products/components/product-list/product-list.component'
import { ProductItemComponent } from './products/components/product-item/product-item.component'
import { ReactiveFormsModule } from '@angular/forms'
import { FormlyModule } from '@ngx-formly/core'
import { FormlyMaterialModule } from '@ngx-formly/material'

import { FormlyMatDatepickerModule } from '@ngx-formly/material/datepicker'
import { FormlyMatToggleModule } from '@ngx-formly/material/toggle'
import { MatDatepickerModule } from '@angular/material/datepicker'
import { MatDialogModule } from '@angular/material/dialog'
import { MatFormFieldModule } from '@angular/material/form-field'
import { MatInputModule } from '@angular/material/input'
import { MatRadioModule } from '@angular/material/radio'
import { MatSelectModule } from '@angular/material/select'
import { MatCheckboxModule } from '@angular/material/checkbox'
import { MatNativeDateModule } from '@angular/material/core'
import { ProductAdminComponent } from './products/components/product-admin/product-admin.component'

@NgModule({
    declarations: [
        AppComponent,
        ProductListComponent,
        ProductItemComponent,
        ProductItemComponent,
        ProductAdminComponent,
    ],
    imports: [
        BrowserModule,
        HttpClientModule,
        AppRoutingModule,
        BrowserAnimationsModule,
        MatButtonModule,
        ReactiveFormsModule,
        FormlyModule.forRoot(),
        FormlyMaterialModule,
        ReactiveFormsModule,
        MatCheckboxModule,
        MatDatepickerModule,
        MatDialogModule,
        MatFormFieldModule,
        MatInputModule,

        MatRadioModule,
        MatSelectModule,

        MatNativeDateModule,
        FormlyMatDatepickerModule,
        FormlyMatToggleModule,
    ],
    providers: [],
    bootstrap: [AppComponent],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Let’s build our first form.


3. Product overview

Like with most admin pages, we want to show a list of all the products we have. For every product we want to add product actions buttons like edit and remove.

We will use the material table for that. For this we need to import the ​MatTableModule​ in the ​app.module.ts​.

//... all the other imported modules
import { MatTableModule } from '@angular/material/table'

@NgModule({
    declarations: [//...],
    imports: [
        //...
        MatTableModule,
    ],
    providers: [],
    bootstrap: [AppComponent],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Now we can add the table to our ​product-item​ component and get the data from our serverless function with the ​ProductService​ in Angular.

import { Component, OnInit } from ' @angular/core'
import { ProductData } from '../../models/product'
import { ProductService } from '../../service/product.service'

@Component({
    selector: 'app-product-admin',
    templateUrl: './ product-admin.component.html',
    styleUrls: ['./product-admin.component.scss'],
})
export class ProductAdminComponent implements OnInit {
    public products: ProductData[] = []
    public displayedColumns: string[] = ['id', 'name', 'price', 'actions']
    public dataSource = null

    constructor(private productService: ProductService) {}

    ngOnInit(): void {
        console.log('dataSource: ', this.dataSource)
        this.productService.getProducts().then((products: ProductData[]) => {
            console.log(products)

            this.products = products
            this.dataSource = products
            console.log('dataSource: ', this.dataSource)
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

In the ​product-admin.component.html​ we add the table to show all the data in the right columns.

<header class="admin__header">
    <h1>Products admin</h1>
    <button mat-flat-button color="primary">New product</button>
</header>

<mat-table [dataSource]="dataSource">
    <!-- ID Column -->

    <ng-container matColumnDef="id">
        <mat-header-cell *matHeaderCellDef> ID </mat-header-cell>
        <mat-cell *matCellDef="let element"> {{ element.id }} </mat-cell>
    </ng-container>

    <!-- Name Column -->

    <ng-container matColumnDef="name">
        <mat-header-cell *matHeaderCellDef> Name </mat-header-cell>
        <mat-cell *matCellDef="let element"> {{ element.name }} </mat-cell>
    </ng-container>

    <!-- Price Column -->

    <ng-container matColumnDef="price">
        <mat-header-cell *matHeaderCellDef> Price </mat-header-cell>
        <mat-cell *matCellDef="let element"> {{ element.price }} </mat-cell>
    </ng-container>

    <ng-container matColumnDef="actions">
        <mat-header-cell *matHeaderCellDef>Action</mat-header-cell>
        <mat-cell *matCellDef="let element">
            <button [routerLink]="['/admin/product/', element.id]" mat-flat-button color="primary">Edit</button>
        </mat-cell>
    </ng-container>

    <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
    <mat-row *_matRowDef="let row; columns:displayedColumns"></mat-row>
</mat-table>
Enter fullscreen mode Exit fullscreen mode

We can add some CSS to improve the styling.

:host {
    width: 100%;
}

.admin {
    &__header {
        margin-bottom: 1rem;
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Create a product

We need a view that will display a form to create or update a product. So let’s generate a component for that and add it to the routing module.

ng generate component products/components/product-form
Enter fullscreen mode Exit fullscreen mode

In the routing module we add a few routes.

import { NgModule } from '@angular/core'
import { Routes, RouterModule } from '@angular/router'
import { ProductListComponent } from './products/components/product-list/product-list.component'
import { ProductItemComponent } from './products/components/product-item/product-item.component'
import { ProductAdminComponent } from './products/components/product-admin/product-admin.component'
import { ProductFormComponent } from './products/components/product-form/product-form.component'

const routes: Routes = [
    {
        path: '',
        component: ProductListComponent,
    },
    {
        path: 'product/:id',
        component: ProductItemComponent,
    },
    {
        path: 'admin',
        component: ProductAdminComponent,
    },
    {
        path: 'admin/product/:id',
        component: ProductFormComponent,
    },
    {
        path: '**',
        redirectTo: '',
    },
]

@NgModule({
    imports: [RouterModule.forRoot(routes)],
    exports: [RouterModule],
})
export class AppRoutingModule {}
Enter fullscreen mode Exit fullscreen mode

Example admin page

If you watch the admin page and click the edit button you should get a URL like this “http://localhost:4200/admin/product/266790280843231752​” but there is no form yet. So let’s build that form to show the product information in it.

To get the product ID from the URL we need the ​ActivatedRoute​ module in our constructor from the ​ProductFormComponent​. In the ​ngOnInit​ we need that product ID to get all the data from our product. But in our case we use this component also for
showing a form when creating a new product.

import { Component, OnInit } from '@angular/core'
import { ProductData } from '../../models/product'
import { ProductService } from '../../service/product.service'
import { ActivatedRoute } from '@angular/router'
import { FormGroup } from '@angular/forms'
import { FormlyFieldConfig } from '@ngx-formly/core'

@Component({
    selector: 'app-product-form',
    templateUrl: './product-form.component.html',
    styleUrls: ['./product-form.component.scss'],
})
export class ProductFormComponent implements OnInit {
    public id: string = ''
    public productItem: ProductData = null

    public productProps: string[] = []

    public form = new FormGroup({})
    public model = {}
    public fields: FormlyFieldConfig[] = [
        {
            key: 'name',
            type: 'input',
            templateOptions: {
                label: 'Name',
                placeholder: 'Enter name',
                required: true,
            },
        },
        {
            key: 'description',
            type: 'input',
            templateOptions: {
                type: 'text',

                label: 'Description',
                placeholder: 'Enter description',
                required: true,
            },
        },
        {
            key: 'price',
            type: 'input',
            templateOptions: {
                type: 'number',
                label: 'Price',
                placeholder: 'Enter price',
                required: true,
            },
        },
        {
            key: 'quantity',
            type: 'input',
            templateOptions: {
                typpe: 'number',
                label: 'Quantity',
                placeholder: 'Enter quantity',
                required: true,
            },
        },
        {
            key: 'backorderLimit',
            type: 'input',

            templateOptions: {
                typpe: 'number',
                label: 'Backorder limit',
                placeholder: 'Enter backorderLimit',
                required: true,
            },
        },
        {
            key: 'backordered',
            type: 'checkbox',
            templateOptions: {
                label: 'Backordered',
                placeholder: 'Enter backordered',
                required: true,
            },
        },
    ]

    constructor(private product: ProductService, private route: ActivatedRoute) {
        this.route.params.subscribe((params) => {
            this.id = params?.id
        })
    }

    public ngOnInit(): void {
        this.getProduct()
    }

    private getProduct() {
        if (this.id !== 'new') {
            this.product.getProductById(this.id).then((product) => {
                this.productItem = product
            })
        } else {
            this.productItem = new ProductData()
        }
    }

    public onSubmit(data) {
        console.log(data)
    }
}
Enter fullscreen mode Exit fullscreen mode

For the forms we use NGX-formly like we installed a few steps back. Now we need to create a ​FormGroup​ and a ​fields​ array where we configure all the fields that we want in our form.

The great thing about NGX-formly is that we only have to add a ​<form>​ and <formly> elements. In the ​​ element we add fields and the model. The fields will automatically be created by formly. The model is used to show the data for an existing
product.

<div class="form__wrapper">
    <form [formGroup]="form" (ngSubmit)="onSubmit(productItem)">
        <formly-form [form]="form" [fields]="fields" [model]="productItem"></formly-form>
        <button mat-flat-button color="primary" type="submit" class="btn btn-default">Submit</button>
    </form>
</div>
Enter fullscreen mode Exit fullscreen mode

The result should look something like this. But I can imagine that you want to change the styling to make it more pleasing to the eyes of user.

Alt Text

Now that we have prepared the frontend with the edit view, we need to create a serverless function that will save the data for both a new and existing product.

In the ​product-service.js​ I add a new method to post the data for a new product.

createNewProduct(product) {
    return new Promise((resolve, reject) => {
        if (!product) {
            reject('No product data provided')
        }

        this.client
            .query(
                q.Create(q.Collection('products'), {
                    data: product,
                }),
            )
            .then((result) => {
                resolve(result)
            })
            .catch((error) => {

                console.log('createNewProduct', error)

                reject(error)
            })
    })
}
Enter fullscreen mode Exit fullscreen mode

For the serverless function I create a new file ​product-new.js​ which will result in a new endpoint ​/product-new​.

import { ProductService } from '../lib/product-service.js'
import { client, headers } from '../lib/config.js'

const service = new ProductService({ client })

exports.handler = async (event, context) => {
    console.log('Function `product-new` invoked')

    const { body } = event

    if (event.httpMethod === 'OPTIONS') {
        return { statusCode: 200, headers, body: 'Ok' }
    }

    const parsedBody = JSON.parse(body)
    if (!parsedBody) {
        return {
            statusCode: 400,
            headers,
            body: JSON.stringify({
                message: 'Some product data is missing', parsedBody }),
        }
    }

    if (event.httpMethod !== 'POST') {
        return {
            statusCode: 405, headers, body: 'Method Not Allowed' }
        }

        try {
            const product = await
                service.createNewProduct(parsedBody)
            return {

                statusCode: 200,
                headers,
                body: JSON.stringify(product),
            }
        } catch (error) {
            console.log('error', error)

            return {
                statusCode: 400,
                headers,
                body: JSON.stringify(error),
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In this function I check if there is product data in the body and if that body has data. Otherwise, it will return an error. To test if it accepts my data I test it locally via ​Insomnia
(​Postman​ is also a great tool to test your API).

When you send a ​POST​ request from Anguar it will first send an ​OPTIONS​ request. For now we accept all of them, but you should make this secure.

This is the data I used to test the endpoint:

{
    "name": "iPhone 12",
    "description": "The newest iPhone",
    "price": 1299,
    "quantity": 2000,
    "backorderLimit": 10,
    "backordered": false,
    "image": "https://images.unsplash.com/photo-1577937927133-66ef06acdf18?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=0&q=80"
}
Enter fullscreen mode Exit fullscreen mode

Now that we see the API endpoint works we can connect it in our Angular application. We replace the ​onSubmit​ method in the product-form component.

In the Angular product-service we add a method for sending the request to the serverless function.

//.... ProductService
createNewProduct(product) {
    return new Promise((resolve, reject) => {
        if (!product) {
            reject('No product data provided')
        }

        this.client
            .query(
                q.Create(q.Collection('products'), {
                    data: product,
                }),
            )
            .then((result) => {
                resolve(result)
            })
            .catch((error) => {
                console.log('createNewProduct', error)

                reject(error)
            })
    })
}
//...

//.... ProductFormComponent

public async onSubmit(data) {
    console.log(data)
    const newProduct = await
    this.product.createNewProduct(data)
    if (newProduct) {
        this.router.navigate(['/admin'])
    }
}
//....
Enter fullscreen mode Exit fullscreen mode

When you check your browser, fill in the form, click on the submit button you should be able to create a new product. After the creation is done you will be redirected to the
admin page.

Alt Text

Alt Text

Check the ​ Github repo ​ for the complete code.


4. Update a product

Now that we can create a product, we also want to update some of its information. Let’s create a product update serverless function. Be aware that you only have to send the changed fields from a product instead of sending all of them.

In the product service for the serverless function we create the update method. To check which fields are changed compared to the existing product I created a method to filter out the unchanged fields.

import faunadb from 'faunadb'
const q = faunadb.query

export class ProductService {
    // Code from previous steps ....
    async updateProduct(productId, product) {
        const filterdProduct = await this.filterUnChangedKeys(product)

        return new Promise((resolve, reject) => {
            if (!product || !filterdProduct) {
                reject('No product data provided')
            }

            this.client
                .query(q.Update(q.Ref(q.Collection('products'), productId), { data: filterdProduct }))
                .then((result) => {
                    resolve(result)
                })
                .catch((error) => {
                    console.log('updateProduct', error)

                    reject(error)
                })
        })
    }

    async filterUnChangedKeys(product) {
        const originalProduct = await this.getProductById(product.id)
        return new Promise((resolve, reject) => {
            if (!originalProduct) {
                reject(originalProduct)
            }
            const tempProduct = {}
            for (const key in product) {
                const value = product[key]
                if (value !== originalProduct.data[key] && key !== 'id' && key !== 'storehouse') {
                    tempProduct[key] = value
                }
            }
            resolve(tempProduct)
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

In the directory ​functions/product-update.js​ we create the serverless function where we call the service.

import { ProductService } from '../lib/product-service.js'
import { client, headers } from '../lib/config.js'

const service = new ProductService({ client })

exports.handler = async (event, context) => {
    console.log('Function `product-update` invoked')

    const { body, path } = event
    const productId = path.substr(path.lastIndexOf('/') + 1)

    if (event.httpMethod === 'OPTIONS') {
        return { statusCode: 200, headers, body: 'Ok' }
    }

    const parsedBody = JSON.parse(body)

    if (!parsedBody) {
        return {
            statusCode: 400,
            headers,
            body: JSON.stringify({
                message: 'Some product data is missing',
                parsedBody,
            }),
        }
    }

    if (event.httpMethod !== 'PUT') {
        return {
            statusCode: 405,
            headers,
            body: 'Method Not Allowed',
        }
    }

    try {
        let product = null
        if (event.httpMethod === 'PUT' && productId) {
            product = await service.updateProduct(productId, parsedBody)
        }
        return {
            statusCode: 200,
            headers,
            body: JSON.stringify(product),
        }
    } catch (error) {
        console.log('error', error)

        return {
            statusCode: 400,
            headers,
            body: JSON.stringify(error),
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we can use the same form in the frontend to change product information. We made the product form smart with NGX-Formly to show the values when available. In the submit method we now have to choose if it’s a new product or an existing product (​product-form.component.ts​).

public async onSubmit(data) {
    let product = this.id === 'new' ? await
    this.product.createNewProduct(data) : await
    this.product.updateProduct(this.id, data)
    if (product) {
        this.router.navigate(['/admin'])
    }
}
Enter fullscreen mode Exit fullscreen mode

If you test to update one of your products it should work.

Alt Text

Check the ​ Github repo ​ for the complete code


4. Delete a product

Of course we want to delete a product as well. Let’s create a serverless function for deleting a product. In the service for the serverless functions we add a method to call the FaunaDB API to delete the product.

async deleteProduct(productId) {
    return new Promise((resolve, reject) => {

        if (!productId) {
            reject('No product ID provided')
        }

        this.client
            .query(q.Delete(q.Ref(q.Collection('products'),
                productId)))
            .then((result) => {
                resolve('OK')
            })
            .catch((error) => {
                console.log('deleteProduct', error)

                reject(error)
            })
    })
}
Enter fullscreen mode Exit fullscreen mode

The serverless function ​functions/product-delete.js​ will look like this.

import { ProductService } from '../lib/product-service.js'
import { client, headers } from '../lib/config.js'

const service = new ProductService({ client })

exports.handler = async (event, context) => {
    console.log('Function `product-delete` invoked')

    const { path } = event
    const productId = path.substr(path.lastIndexOf('/') + 1)

    if (event.httpMethod === 'OPTIONS') {
        return { statusCode: 200, headers, body: 'Ok' }
    }

    if (event.httpMethod !== 'DELETE') {
        return {
            statusCode: 405,
            headers,
            body: 'Method Not Allowed',
        }
    }

    try {
        let product = null
        if (event.httpMethod === 'DELETE' && productId) {
            product = await service.deleteProduct(productId)
        }

        return {
            statusCode: 200,
            headers,
            body: JSON.stringify(product),
        }
    } catch (error) {
        console.log('error', error)

        return {
            statusCode: 400,
            headers,
            body: JSON.stringify(error),
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

If you call the serverless function via Postman or Insomnia with a ​DELETE​ method the response body should be ​OK​ with this url: "http://localhost:9000/.netlify/functions/product-delete/PRODUCT_ID​"

Now we can add the delete functionality in the admin page. The edit button we added before is going to be changed. I think adding an icon is a bit more clear for the user-experience.

Add the ​MatIcon​ module to the ​app.module.ts​ to use it.

import { BrowserModule } from '@angular/platform-browser'
import { NgModule } from '@angular/core'

import { AppRoutingModule } from './app-routing.module'
import { AppComponent } from './app.component'
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
import { MatButtonModule } from '@angular/material/button'

import { HttpClientModule } from '@angular/common/http'
import { ProductListComponent } from './products/components/product-list/product-list.component'
import { ProductItemComponent } from './products/components/product-item/product-item.component'
import { ReactiveFormsModule } from '@angular/forms'
import { FormlyModule } from '@ngx-formly/core'
import { FormlyMaterialModule } from '@ngx-formly/material'

import { FormlyMatDatepickerModule } from '@ngx-formly/material/datepicker'
import { FormlyMatToggleModule } from '@ngx-formly/material/toggle'
import { MatDatepickerModule } from '@angular/material/datepicker'
import { MatDialogModule } from '@angular/material/dialog'
import { MatFormFieldModule } from '@angular/material/form-field'
import { MatInputModule } from '@angular/material/input'
import { MatRadioModule } from '@angular/material/radio'
import { MatSelectModule } from '@angular/material/select'
import { MatCheckboxModule } from '@angular/material/checkbox'
import { MatNativeDateModule } from '@angular/material/core'
import { MatTableModule } from '@angular/material/table'
// MatIconModule import
import { MatIconModule } from '@angular/material/icon'

import { ProductAdminComponent } from './products/components/product-admin/product-admin.component'
import { ProductFormComponent } from './products/components/product-form/product-form.component'

@NgModule({
    declarations: [
        AppComponent,
        ProductListComponent,
        ProductItemComponent,
        ProductItemComponent,
        ProductAdminComponent,
        ProductFormComponent,
    ],
    imports: [
        BrowserModule,
        HttpClientModule,
        AppRoutingModule,
        BrowserAnimationsModule,
        MatButtonModule,
        ReactiveFormsModule,
        FormlyModule.forRoot(),
        FormlyMaterialModule,
        ReactiveFormsModule,
        MatCheckboxModule,
        MatDatepickerModule,
        MatDialogModule,
        MatFormFieldModule,
        MatInputModule,
        MatRadioModule,
        MatSelectModule,
        MatTableModule,
        // MatIconModule import
        MatIconModule,

        MatNativeDateModule,
        FormlyMatDatepickerModule,
        FormlyMatToggleModule,
    ],
    providers: [],
    bootstrap: [AppComponent],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

In the ​product-admin.component.html​ we can change the edit button and add a new one for deleting products.

<header class="admin__header">
    <h1>Products admin</h1>
    <button [routerLink]="['/admin/product/new']" mat-flat-button color="secondary">New product</button>
</header>

<mat-table [dataSource]="dataSource">
    <!-- ID Column -->

    <ng-container matColumnDef="id">
        <mat-header-cell *matHeaderCellDef> ID </mat-header-cell>
        <mat-cell *matCellDef="let element"> {{ element.id }} </mat-cell>
    </ng-container>

    <!-- Name Column -->

    <ng-container matColumnDef="name">
        <mat-header-cell *matHeaderCellDef> Name </mat-header-cell>
        <mat-cell *matCellDef="let element"> {{ element.name }} </mat-cell>
    </ng-container>

    <!-- Price Column -->

    <ng-container matColumnDef="price">
        <mat-header-cell *matHeaderCellDef> Price </mat-header-cell>
        <mat-cell *matCellDef="let element"> {{ element.price }} </mat-cell>
    </ng-container>

    <ng-container matColumnDef="actions">
        <mat-header-cell *matHeaderCellDef>Action</mat-header-cell>
        <mat-cell *matCellDef="let element">
            <button
                [routerLink]="['/admin/product/', element.id]"
                mat-icon-button
                color="primary"
                aria-label="Edit product"
            >
                <mat-icon>edit</mat-icon>
            </button>
            <button mat-icon-button color="error" aria-label="Delete product">
                <mat-icon>delete</mat-icon>
            </button>
        </mat-cell>
    </ng-container>

    <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
    <mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
</mat-table>
Enter fullscreen mode Exit fullscreen mode

If you get all kinds of error’s in the frontend, please restart the Angular development server, it might not have imported the MatIconModule correctly.

The result in the browser should look something like this.

Alt Text

In the ​product.service.ts​ we define a method that calls the delete serverless function.

public async deleteProduct(productId: string) {
    if (!productId) return

    let product = null

    try {
        product = await this.http.delete<Product>(environment.apiUrl + 'product-delete/' + productId).toPromise()
    } catch (error) {
        console.error('error: ', error)
        return error

    }
    return product
}
Enter fullscreen mode Exit fullscreen mode

Now we can call this method in our ​product-admin.component.ts​ so we can call it via the

the click of a delete button. Since we want to get all new data after we delete a product, we have to create a method which does all of this. So we can re-use it in the ​ngOnInit() and ​deleteProduct()​ methods.

import { Component, OnInit } from '@angular/core'
import { ProductData } from '../../models/product'
import { ProductService } from '../../service/product.service'
import { Router } from '@angular/router'

@Component({
    selector: 'app-product-admin',
    templateUrl: './product-admin.component.html',
    styleUrls: ['./product-admin.component.scss'],
})
export class ProductAdminComponent implements OnInit {
    public products: ProductData[] = []
    public displayedColumns: string[] = ['id', 'name', 'price', 'actions']
    public dataSource = null

    constructor(private productService: ProductService, private router: Router) {}

    ngOnInit(): void {
        console.log('dataSource: ', this.dataSource)
        this.getProductData()
    }

    deleteProduct(productId: string): void {
        this.productService
            .deleteProduct(productId)
            .then((result) => {
                this.getProductData()
            })
            .catch((error) => {
                console.log(error)
            })
    }

    getProductData(): void {
        this.productService.getProducts().then((products: ProductData[]) => {
            console.log(products)
            this.products = products
            this.dataSource = products
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

In the ​product-admin.component.html​ we add a click handler to the delete button.

<header class="admin__header">
    <h1>Products admin</h1>
    <button [routerLink]="['/admin/product/new']" mat-flat-button color="secondary">New product</button>
</header>

<mat-table [dataSource]="dataSource">
    <!-- ID Column -->

    <ng-container matColumnDef="id">
        <mat-header-cell *matHeaderCellDef> ID </mat-header-cell>
        <mat-cell *matCellDef="let element"> {{ element.id }} </mat-cell>
    </ng-container>

    <!-- Name Column -->

    <ng-container matColumnDef="name">
        <mat-header-cell *matHeaderCellDef> Name </mat-header-cell>
        <mat-cell *matCellDef="let element"> {{ element.name }} </mat-cell>
    </ng-container>

    <!-- Price Column -->

    <ng-container matColumnDef="price">
        <mat-header-cell *matHeaderCellDef> Price </mat-header-cell>
        <mat-cell *matCellDef="let element"> {{ element.price }} </mat-cell>
    </ng-container>

    <ng-container matColumnDef="actions">
        <mat-header-cell *matHeaderCellDef>Action</mat-header-cell>
        <mat-cell *matCellDef="let element">
            <button
                [routerLink]="['/admin/product/', element.id]"
                mat-icon-button
                color="primary"
                aria-label="Edit product"
            >
                <mat-icon>edit</mat-icon>
            </button>
            <button mat-icon-button color="error" aria-label="Delete product" (click)="deleteProduct(element.id)">
                <mat-icon>delete</mat-icon>
            </button>
        </mat-cell>
    </ng-container>

    <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
    <mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
</mat-table>
Enter fullscreen mode Exit fullscreen mode

Test it in the browser! In my experience the combination of the easy FaunaDB API and
the Netlify serverless functions are working as fast as a rocket .

Check the Github repo for the complete code


5. Security

Be aware that I didn’t implement a security layer yet. For that reason I won’t deploy this version to my test environment. In the next step we are gonna build our user
authentication.

In the meantime play with everything until my next step will be published.

I think you should be very proud of the functionality to create, edit and delete a product. By now I think you would agree with me that Serverless Functions talking to the
FaunaDB database isn’t all that difficult.

Happy Coding ​🚀

Top comments (0)