articles

Home / DeveloperSection / Articles / CRUD operation in knockoutjs using AJAX.

CRUD operation in knockoutjs using AJAX.

CRUD operation in knockoutjs using AJAX.

Ravi Vishwakarma 31 27-Jun-2024

Creating a CRUD (Create, Read, Update, Delete) application using KnockoutJS with AJAX involves several steps. Below is a detailed guide to help you through the process:

1. Set Up Your Project

First, set up your project by including the necessary libraries and creating the basic HTML structure.

HTML Structure

<!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>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/knockout-validation/2.0.4/knockout.validation.min.js"
        integrity="sha512-b99MDNv5TqiZtPKH2UeHzDAVydmgrOJEPtaPPEF8AgV86eYyqINFI/K7/7f0+R4WNTAVv8KvvpjwfOYHv5rd5g=="
        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>
    <style type="text/css">
        .waitMe {
            z-index: 99999;
        }
    </style>
</head>

<body>
    <div class="position-absolute top-0 end-0 bottom-0 start-0 text-center bg-dark bg-opacity-10 waitMe d-none">
        <div class="d-flex justify-content-center align-content-center align-items-center h-100">
            <span class="mx-3 fw-bold">Loading . . .</span>
            <div class="spinner-border" role="status">
                <span class="visually-hidden">Loading...</span>
            </div>
        </div>
    </div>
    <!-- Main container for the content -->
    <div class="container my-3">
        <!-- Header for the section -->
        <h2>Data from Server</h2>

        <div class="d-flex w-100 justify-content-between ">
            <!-- Button trigger modal -->
            <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#postStaticBackdrop">
                New Post
            </button>
            <div>
                <input type="search" data-bind="value: searchText, valueUpdate: 'input'" class="form-control"
                    placeholder="Search ..." />
            </div>
        </div>
        <hr />
        <p class="fw-semibold text-black-50 text-end">
            <span class="" data-bind="text: filteredItemsLength"></span>
            <span>data found.</span>
        </p>
        <div data-bind="css: { 'd-none': filteredItemsLength() !== 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>
        <!-- Responsive row to display items fetched from the server -->
        <div class="row g-3 row-cols-1 row-cols-md-2 row-cols-lg-4 row-cols-xl-4" data-bind="foreach: filteredItems">
            <!-- Column for each item -->
            <div class="col">
                <!-- Card to display item details -->
                <div class="card h-100">
                    <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 -->
                        <div class="m-0 d-flex justify-content-between">
                            <!-- Item id displayed -->
                            <span class="fs-5 fw-bold">
                                <span data-bind="text: $index"></span>
                                <span class="text-black-50">
                                    (<small class="fs-6 fw-bold" data-bind="text: id"></small>)
                                </span>
                            </span>
                            <div class="">
                                <button class="btn btn-link text-primary pe-0"
                                    data-bind="click: $parent.editPost">Edit</button>
                                <button class="btn btn-link text-danger pe-0"
                                    data-bind="click: $parent.deletePost">Delete</button>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</body>
<!-- Modal -->
<div class="modal fade" id="postStaticBackdrop" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1"
    aria-labelledby="postStaticBackdropLabel" aria-hidden="true">
    <div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
        <div class="modal-content">
            <div class="modal-header">
                <h1 class="modal-title fs-5" id="postStaticBackdropLabel">Add New Post</h1>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body">
                <form class="row g-3"
                    data-bind="submit: function() { addPost({ title: newTitle(), body: newBody(), userId: userId(), id: id() }); }">
                    <input type="hidden" data-bind="value: id" />
                    <input type="hidden" data-bind="value: userId" />
                    <div class="col-12">
                        <label class="form-label fw-bold">Title</label>
                        <input type="text" placeholder="Title" id="newTitle" name="newTitle"
                            data-bind="value: newTitle, valueUpdate: 'input'" class="form-control">
                    </div>
                    <div class="col-12">
                        <label class="form-label fw-bold">Body</label>
                        <textarea rows="5" placeholder="Body" id="newBody" name="newBody"
                            data-bind="value: newBody, valueUpdate: 'input'" class="form-control"></textarea>
                    </div>
                    <div class="col-12">
                        <button type="submit" class="btn btn-primary px-5 rounded-pill">Save</button>
                    </div>
                </form>
            </div>
        </div>
    </div>
</div>


<!-- Link to external JavaScript file -->
<script src="CRUD-with-js.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
    integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
    crossorigin="anonymous"></script>

</html>

2. Create the ViewModel

Create a JavaScript file (e.g., CRUD-with-js.js) and define your ViewModel.

ViewModel Definition

var loader = $('.waitMe');

function toggleLoader(){
    if(loader.hasClass('d-none')){
        loader.removeClass('d-none');
    }else{
        loader.addClass('d-none');
    }
}


// app.js
// Initialize Knockout validation
ko.validation.init({
    registerExtenders: true, // Register custom validation rules
    messagesOnModified: true, // Show validation messages as soon as a field is modified
    insertMessages: true, // Insert validation messages next to the input elements
    parseInputAttributes: true, // Parse HTML5 input attributes for validation rules
    errorClass: 'text-danger fw-semibold', // CSS class for validation error messages
    messageTemplate: null // Use default message template
}, true);

class AppViewModel {
    constructor() {
        var self = this;

        // Observable array to hold the data fetched from the server
        self.items = ko.observableArray([]);
        self.searchText = ko.observable('');
        self.newTitle = ko.observable('').extend({
            required: {
                message: "Title is required."
            },
            minLength: {
                params: 2,
                message: "Title must be at least 2 characters."
            }
        });
        self.newBody = ko.observable('').extend({
            required: {
                message: "Body is required."
            },
            minLength: {
                params: 2,
                message: "Body must be at least 2 characters."
            }
        });

        self.userId = ko.observable('11');
        self.id = ko.observable(0);
        // Function to fetch data from the API
        self.fetchData = function() {
            $.ajax({
                url: 'https://jsonplaceholder.typicode.com/posts',
                beforeSend: function(xhr) {
                    toggleLoader();
                },
                success: function(result, status, xhr) {
                    if (status === 'success') {
                        self.items(result); // Corrected from 'item' to 'post'
                    }
                },
                error: function(xhr) {
                    alert("An error occurred: " + xhr.status + " " + xhr.statusText);
                },
                complete: function(xhr,status){
                    toggleLoader();
                }
            });
        };


        // Function to add a new post
        self.addPost = function(newPost) {
            // Check if the form is valid
            if (self.errors().length === 0) {
                // Determine if it's an update or a new post
                var isUpdate = newPost.id !== 0;

                // Construct the appropriate URL based on the action
                var url = isUpdate ? `https://jsonplaceholder.typicode.com/posts/${newPost.id}` : 'https://jsonplaceholder.typicode.com/posts';


                $.ajax({
                    url: url,
                    type: isUpdate ? 'PUT' : 'POST',
                    data: JSON.stringify(newPost),
                    headers: {
                        'Content-type': 'application/json; charset=UTF-8',
                    },
                    beforeSend: function(xhr) {
                        toggleLoader();
                    },
                    success: function(result, status, xhr) {
                        if (status === 'success') {
                            // Update the items array with the new or updated post
                            if (isUpdate) {
                                // Find the index of the existing post and update it
                                let index = self.items().findIndex(item => item.id === newPost.id);
                                if (index !== -1) {
                                    self.items.splice(index, 1, newPost);
                                }
                            } else {
                                self.items.push(newPost);
                            }

                            // Clear input fields after successful addition or update
                            self.newTitle('');
                            self.newBody('');
                            self.id(0);

                            // Hide modal using jQuery
                            $('#postStaticBackdrop').modal('hide');

                            // Hide all validation messages
                            self.errors.showAllMessages(false);
                        }
                    },
                    error: function(xhr) {
                        alert("An error occurred: " + xhr.status + " " + xhr.statusText);
                    },
                    complete: function(xhr,status){
                        toggleLoader();
                    }
                });

            } else {
                // Show validation errors
                self.errors.showAllMessages(true);
            }
        };

        // Function to delete a product from the list
        self.deletePost = function(post) {
            if (window.confirm("Are you sure you want to delete this product?")) {
                $.ajax({
                    url: `https://jsonplaceholder.typicode.com/posts/${post.id}`,
                    type: 'DELETE',
                    headers: {
                        'Content-type': 'application/json; charset=UTF-8',
                    },
                    beforeSend: function(xhr) {
                        toggleLoader();
                    },
                    success: function(result, status, xhr) {
                        if (status === 'success') {
                            self.items.remove(post); // Corrected from 'item' to 'post'
                        }
                    },
                    error: function(xhr) {
                        alert("An error occurred: " + xhr.status + " " + xhr.statusText);
                    },
                    complete: function(xhr,status){
                        toggleLoader();
                    }
                });
            }
        };


        // Function to start editing a product
        self.editPost = function(post) {
            self.newTitle(post.title); // Clear input after successful addition
            self.newBody(post.body); // Clear input after successful addition
            self.id(post.id);
            self.userId(post.userId);
            $('#postStaticBackdrop').modal('show');
        };

        // Computed observable to filter the items based on the filter criteria
        self.filteredItems = ko.computed(function() {
            //console.log(self.items().reverse());
            // Reverse the items array
            var reversedItems = self.items().slice().reverse();

            //var filter = self.filter().toLowerCase(); // Convert filter criteria to lowercase
            if (!self.searchText) {
                return reversedItems; // If no filter, return all items
            } else {
                // Filter the items based on the filter criteria
                return ko.utils.arrayFilter(reversedItems, function(item) {
                    return JSON.stringify(item).includes(self.searchText().toLowerCase()); // Case-insensitive match
                });
            }
        });

        // Function to reverse the observable array
        self.reverseArray = function() {
            self.items(self.items().slice().reverse());
        };

        self.filteredItemsLength = ko.computed(function() {
            return self.filteredItems().length;
        });
        self.fetchData();

        // Initialize validation
        self.errors = ko.validation.group(self);
    }
}

// Apply the Knockout bindings to the AppViewModel
ko.applyBindings(new AppViewModel());

Explanation

toggleLoader: These methods handle the visibility of a loader animation, which is often used to indicate ongoing background processes like AJAX requests.

Observables:  Array to hold data fetched from the server. searchText, newTitle, newBody, userId, id: Observables to manage form inputs and validation.
fetchData: Makes an AJAX GET request to fetch data from the API. Toggles the loader before and after the request. Populates the items observable array with the fetched data.
addPost: Validates form inputs. Determines if the action is to create a new post or update an existing one. Makes an AJAX POST/PUT request to add/update the post. Updates the items observable array with the new or updated post. Clears the form inputs and hides the modal on success.
deletePost: Confirms with the user before deleting a post. Makes an AJAX DELETE request to delete the post. Removes the deleted post from the items observable array.
editPost: Populates form inputs with the selected post's data. Opens the modal to allow editing.
filteredItems: Computes the filtered items based on searchText. Reverses the items array to show the latest items first. Filters the items based on the search criteria.
reverseArray: Reverses the order of the items observable array.
filteredItemsLength: Computes the length of the filtered items.

 

Read more 

Conditional html heading tag based on variable in knockout.js

How knockout.js variables different from javascript regular variables

How to integrate Knockout.js with other JavaScript libraries or frameworks?

How would you handle data fetching and updating in a Knockout.js application?


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