blog

Home / DeveloperSection / Blogs / Building Single-Page Applications (SPAs) with Knockout.js and RESTful APIs

Building Single-Page Applications (SPAs) with Knockout.js and RESTful APIs

Building Single-Page Applications (SPAs) with Knockout.js and RESTful APIs

Ravi Vishwakarma 112 26-Jun-2024

Single-page applications (SPAs) provide a seamless user experience by dynamically updating the content of a web page without requiring a full page reload. Knockout.js, with its powerful data-binding and MVVM (Model-View-ViewModel) architecture, is an excellent choice for building SPAs. Combined with RESTful APIs, you can create highly responsive and interactive web applications. This guide will walk you through building an SPA using Knockout.js and RESTful APIs.

Setting Up the Project

Create the Project Structure

knockout-spa/
├── index.html
├── app.js
├── styles.css
├── api/
│   ├── products.json
└── server.js

Create the HTML File

index.html

<!doctype html>
<html lang="en">

<head>
    <!-- Meta tags for character set and viewport configuration -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- Title of the webpage -->
    <title>Bootstrap demo</title>

    <!-- Link to Bootstrap CSS for styling -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"
        integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">

    <!-- Link to Knockout.js library for MVVM pattern support -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.5.1/knockout-latest.min.js"
        integrity="sha512-vs7+jbztHoMto5Yd/yinM4/y2DOkPLt0fATcN+j+G4ANY2z4faIzZIOMkpBmWdcxt+596FemCh9M18NUJTZwvw=="
        crossorigin="anonymous" referrerpolicy="no-referrer"></script>

    <!-- Link to jQuery library -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
</head>

<body>
    <!-- Link to external JavaScript file -->
    <script src="app.js"></script>
</body>

</html>

Create the Knockout.js ViewModel

app.js

// ViewModel for the application
class AppViewModel {
    constructor() {
        var self = this;

        // Observables to store data and state
        self.response = ko.observable({}); // Observable to store the response data
        self.currentPage = ko.observable(0); // Observable to track the current page
        self.totalPages = ko.observable(0); // Observable to track the total number of pages
        self.pagination = ko.observable({}); // Observable to store pagination data

        self.isLoading = ko.observable(false); // Observable to track loading state
        self.error = ko.observable(null); // Observable to store error message

        self.pageNo = ko.observable(self.pagination().current_page ? self.pagination().current_page : 1);

        self.pageNo.subscribe(function(newValue){
            let url = 'https://api.artic.edu/api/v1/artworks?page='+ parseInt(newValue);
            self.fetchData(url);
        });


        // Function to fetch data from the API
        self.fetchData = async function (url = '') {
            self.isLoading(true); // Set loading state to true
            self.error(''); // Clear any previous errors
            let _url = url || 'https://api.artic.edu/api/v1/artworks'; // Default URL
            try {
                // Simulate an asynchronous API call using fetch
                let response = await fetch(_url);
                if (!response.ok) {
                    // If response is not ok, throw an error
                    throw new Error('Network response was not ok ' + response.statusText);
                }
                let result = await response.json(); // Parse response as JSON
                self.response(result); // Update the observable array with the fetched data

                // Extract pagination data from the response and update observables accordingly
                if (result.pagination) {
                    self.pagination(result.pagination);
                }
            } catch (err) {
                // If an error occurs during fetch, set error message
                self.error(err.message);
            } finally {
                // Set loading state to false regardless of success or failure
                self.isLoading(false);
            }
        };

        self.showDetails = function(link) {
            // Add your code to handle details here
            console.log("Link clicked:", link);
        }
    }
}

// Apply bindings
ko.applyBindings(new AppViewModel());

Create the Product Section Template

Modify index.html to include the product list template.

    <!-- Main container for the content -->
    <div class="container my-3">
        <!-- Header for the section -->
        <h2>Data from Server async</h2>
        <!-- Button to fetch data -->
        <button data-bind="click: fetchData('')" class="btn btn-secondary">Fetch Data</button>
        <hr />
        <div id="app">
            <!-- Display loading state -->
            <div data-bind="visible: isLoading">
                <div class="d-flex justify-content-center fw-semibold fs-5 align-items-center">
                    <span class="me-2">Loading...</span>
                    <div class="spinner-border" role="status">
                        <span class="visually-hidden">Loading...</span>
                    </div>
                </div>
            </div>

            <!-- Display error message if any -->
            <div class="d-flex justify-content-center fw-semibold fs-5 align-items-center"
                data-bind="visible: error, text: error" style="color: red;"></div>
            <div class="my-3 d-flex justify-content-end align-items-baseline">
                <form>
                    <div class="mx-3">
                        <input class="form-control" data-bind="value: $root.pageNo, valueUpdate: 'input'"
                            placeholder="page no" />
                    </div>
                </form>

                <!-- Pagination -->
                <span class="fw-semibold me-3" data-bind="with: { pages : pagination()}">
                    <span data-bind="text: pages.current_page"></span>
                    <span class="mx-1">of</span>
                    <span class="" data-bind="text: pages.total_pages"></span>
                </span>

                <ul class="pagination "
                    data-bind="with: { pages : pagination(), prevUrl: pagination().prev_url, nextUrl: pagination().next_url, currentPage: pagination().current_page ? pagination().current_page : 1 }">
                    <!-- First Page -->
                    <li class="page-item" data-bind="css: { 'disabled': !(currentPage > 1) }">
                        <a class="page-link" href="#" data-bind="click: function() { $root.fetchData(''); }">
                            First
                        </a>
                    </li>
                    <!-- Previous Page -->
                    <li class="page-item" data-bind="css: { 'disabled': !(currentPage > 1) }">
                        <a class="page-link" href="#" data-bind="click: function() { $root.fetchData(prevUrl); }, 
                                      enable: (currentPage > 1)">
                            Previous
                        </a>
                    </li>
                    <!-- Current Page -->
                    <li class="page-item active"><a class="page-link" href="#"
                            data-bind="text: currentPage ? currentPage : 1"></a></li>
                    <!-- Next Page -->
                    <li class="page-item" data-bind="css: { 'disabled': !(currentPage < pages.total_pages) }">
                        <a class="page-link" href="#" data-bind="click: function() { $root.fetchData(nextUrl); }">
                            Next
                        </a>
                    </li>
                    <!-- Last Page -->
                    <li class="page-item" data-bind="css: { 'disabled': !(currentPage < pages.total_pages) }">
                        <a class="page-link" href="#"
                            data-bind="click: function() { $root.fetchData('https://api.artic.edu/api/v1/artworks?page='+pages.total_pages); }">
                            Last
                        </a>
                    </li>
                </ul>

            </div>
            <div class="" data-bind="if: (response() && response().data && response().data.length <= 0)">
                <hr />
                <div class="d-flex justify-content-center fw-bold">
                    <div class="text-center">
                        <p class=" fs-2 ">
                            <span class="me-2">😕</span>
                            No records were found
                        </p>
                        <!-- <p class="fw-normal">try somthing new keywords</p> -->
                    </div>
                </div>
            </div>


            <!-- Display data if available -->
            <div class="row g-3 row-cols-1 row-cols-md-2 row-cols-lg-3 row-cols-xl-4"
                data-bind="visible: !isLoading(), foreach: response().data">
                <!-- Column for each item -->
                <div class="col">
                    <!-- Card to display item details -->
                    <div class="card h-100">

                        <!-- Item Image -->
                        <img data-bind="if: (image_id && thumbnail), attr: {
                            src: 'https://www.artic.edu/iiif/2/' + image_id + '/full/400,/0/default.jpg',
                            alt: thumbnail ? thumbnail.alt_text : ''
                        }" class="card-img-top" height="250">
                        <div class="card-body">
                            <!-- Card title bound to item's title -->
                            <h5 class="card-title text-capitalize" data-bind="text: title"></h5>
                            <!-- Card text bound to item's body -->
                            <!-- <p class="card-text" data-bind="text: body"></p> -->
                        </div>
                        <div class="card-footer border-0 pt-0">
 <!-- Footer with item id and a link --> <p class="m-0 d-flex justify-content-between"> <!-- Item id displayed --> <span class="fs-5 fw-bold" data-bind="text: id"></span> <!-- Link to comments page for the item --> <a class="" data-bind="attr: { 'data-link': api_link }, click: function() { $root.showDetails(api_link); }"> <!-- SVG icon inside the link --> <svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" fill="currentColor" class="bi bi-arrow-right-circle-fill" viewBox="0 0 16 16"> <path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0M4.5 7.5a.5.5 0 0 0 0 1h5.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3a.5.5 0 0 0 0-.708l-3-3a.5.5 0 1 0-.708.708L10.293 7.5z" /> </svg> </a> </p> </div> </div> </div> </div> </div> </div>

 

Read more 

Latest from knockout.js

CRUD operation in KnockoutJS.

Knockout.js: Building Dynamic Web Applications

 


Hi, my self Ravi Vishwakarma. I have completed my studies at SPICBB Varanasi. now I completed MCA with 76% form Veer Bahadur Singh Purvanchal University Jaunpur. SWE @ MindStick | Software Engineer | Web Developer | .Net Developer | Web Developer | Backend Engineer | .NET Core Developer

Leave Comment

Comments

Liked By