Persistence

This section covers practices for saving data grid data, including the most popular techniques.

What's new with Version 13

  • Original input: persistence payloads carry the method input exactly as given. Transformations such as casting, formulas and events are repeated by every peer that replays the operation, and internal working fields never travel
  • Identity: rows and columns carry a guid, and deleteRow and deleteColumn broadcast the guids aligned with the positions, deleteRow(rows, guids), so all connected users agree on what was removed
  • Sub-steps are not persisted: the handler receives the public operation only, never the internal steps it runs, such as the nested header updates inside insertColumn
  • View state: zoom, selection, group state, search and pagination are personal and never reach the handler

Documentation

Methods

Method Description
getRowId Get the row ID from a given row number.
getRowId(rowIndex: Number) : String | Number
setRowId Set the row ID for a given row number.
setRowId(rowIndex: Number, rowId: Number | String) : void
getRowById Get the data from a row, or the row object by ID.
getRowById(rowId: Number, element?: Boolean) : object | array

Settings

On the spreadsheet configuration level

Property Description
persistence: function A function to handle all changes in the spreadsheet.
persistence(worksheet: Object, method: String, args: Object) => void
autoId: boolean Automatically generate GUID identifiers for rows

What the handler receives

The handler is called once per public operation, with the method name and its input exactly as given, so a peer or a server can replay it by calling the same method with the same arguments. Transformations such as casting, formula evaluation and events are repeated on the replaying side, and internal working fields never travel. The internal steps an operation runs, for example the nested header updates inside insertColumn, are not reported separately.

Rows and columns carry an identity guid, generated automatically. deleteRow and deleteColumn report the positions with the guids aligned as the second argument, deleteRow(rows, guids), so every connected user agrees on what was removed even after concurrent structural changes. Personal view state, such as zoom, selection, group state, search and pagination, never reaches the handler.

On the worksheet configuration level

Property Description
rows.id: string | number Define the unique identifier for the row
rows.guid: string Identity of the row, generated automatically. Do not overwrite it

Examples

General persistence implementation

Use the persistence property to send all spreadsheet changes to your server.

<html>
<script src="https://jspreadsheet.com/v13/jspreadsheet.js"></script>
<script src="https://jsuites.net/v6/jsuites.js"></script>
<link rel="stylesheet" href="https://jspreadsheet.com/v13/jspreadsheet.css" type="text/css" />
<link rel="stylesheet" href="https://jsuites.net/v6/jsuites.css" type="text/css" />

<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Material+Icons" />

<div id="spreadsheet"></div>

<script>
// You can use the following license for quick testing on localhost, StackBlitz, or CodeSandbox.
// The license is valid for one day, after which the spreadsheet will become read-only.
// For a longer trial period, you can create a free account and generate a demo license with an extended expiration date.
jspreadsheet.setLicense('NzdkNzc0ZjhlN2Q1YjRhZWI4ZWM1N2U4ODdiYzE1Y2UzMjZkZWNiMTdmZjU5NzI1MTM0N2NkY2Y5MDhhZWZlYTc2ZGRiY2JlMTg0NWZkYjc5MTRiNzE4MjI4ZDA0ZjI4YjRmMGMwNWQ1ZmFlNzhjMDI3YjBhODBhZDYzZWMyNTgsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94Tnprd01qRXlPRE15TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=');

const data = [
    {
        "title": "Unlocking Android",
        "pageCount": 416,
        "publishedDate": "2009-04-01" ,
        "authors": "W. Frank Ableson, Charlie Collins, Robi Sen",
        "categories": "Open Source;Mobile"
    },
    {
        "title": "Android in Action, Second Edition",
        "pageCount": 592,
        "publishedDate": "2011-01-14",
        "authors": "W. Frank Ableson, Robi Sen",
        "categories": "Java"
    },
    {
        "title": "Flex 3 in Action",
        "pageCount": 576,
        "publishedDate": "2009-02-02",
        "authors": "Tariq Ahmed with Jon Hirschi, Faisal Abid",
        "categories": "Internet"
    },
];

/**
 * Aggregate all the changes in the same row
 */
const aggregateRows = function(worksheet, data) {
    let rows = [];
    for (let i = 0; i < data.length; i++) {
        // Coords
        let x = data[i].x;
        let y = data[i].y;
        // Create row
        if (! rows[y]) {
            rows[y] = {
                id: worksheet.rows[y].id,
                row: y,
                data: {},
            }
        }
        // Key
        let key = worksheet.options.columns[x].name;
        // Data
        rows[y].data[key] = data[i].value;
    }

    // Filter rows
    return rows.filter(function(row) {
        return row != null;
    });
}

// Create the spreadsheet
jspreadsheet(document.getElementById('spreadsheet'), {
    worksheets: [
        {
            autoId: true,
            data: data,
            columns: [
                {
                    type: 'text',
                    title: 'Title',
                    name: 'title',
                    width: '150px',
                },
                {
                    type: 'text',
                    width: '55px',
                    title: 'Pages',
                    name: 'pageCount'
                },
                {
                    type: 'calendar',
                    width: '90px',
                    title: 'Published',
                    name: 'publishedDate'
                },
                {
                    type: 'text',
                    title: 'Author',
                    name: 'authors',
                    width: '150px',
                },
                {
                    type: 'dropdown',
                    title: 'Categories',
                    name: 'categories',
                    source: [
                        'Internet',
                        'Web Development',
                        'Java',
                        'Mobile',
                        'Open Source'
                    ],
                    width: '200px',
                    render: 'tag',
                    multiple: true
                },
            ],
            rows: [
                { id: '4ab2b86b-6b48-4234-82c9-ea614374fb0b' },
                { id: 'ce9fbb3a-2330-425d-9a81-e75b711bec8d' },
                { id: 'd57533c3-d181-4083-b999-b8bc58bbf236' }
            ]
        }
    ],
    persistence: function(worksheet, method, payload) {
        let ignore = ['setBorder', 'resetBorders' ];
        // Ignore the methods to update the borders
        if (ignore.indexOf(method) >= 0) {
            return false;
        } else {
            // Aggregate rows for the following methods
            if (method === 'setValue' || method === 'setFormula') {
                payload = aggregateRows(worksheet, payload.data)
            }
            // Create attributes to be sent
            let formData = new FormData();
            formData.append('data', JSON.stringify(payload));
            // Fetch options
            const fetchOptions = {
                method: 'POST',
                headers: {
                    'X-Requested-With': 'http',
                    'Authorization': 'Bearer your-token',
                },
                body: formData,
            };
            // Perform the fetch
            fetch('/save', fetchOptions).then(response => response.json()).then(function (result) {
                // Save your data remotely
            }).catch(function (result) {
                // Something went wrong
            });
        }
    }
});
</script>
</html>
import React, { useRef } from "react";
import { Spreadsheet, Worksheet } from "@jspreadsheet/react";
import "jsuites/dist/jsuites.css";
import "jspreadsheet/dist/jspreadsheet.css";

// You can use the following license for quick testing on localhost, StackBlitz, or CodeSandbox.
// The license is valid for one day, after which the spreadsheet will become read-only.
// For a longer trial period, you can create a free account and generate a demo license with an extended expiration date.
jspreadsheet.setLicense('NzdkNzc0ZjhlN2Q1YjRhZWI4ZWM1N2U4ODdiYzE1Y2UzMjZkZWNiMTdmZjU5NzI1MTM0N2NkY2Y5MDhhZWZlYTc2ZGRiY2JlMTg0NWZkYjc5MTRiNzE4MjI4ZDA0ZjI4YjRmMGMwNWQ1ZmFlNzhjMDI3YjBhODBhZDYzZWMyNTgsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94Tnprd01qRXlPRE15TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=');

/**
 * Aggregate all the changes in the same row
 */
const aggregateRows = function(worksheet, data) {
    let rows = [];
    for (let i = 0; i < data.length; i++) {
        // Coords
        let x = data[i].x;
        let y = data[i].y;
        // Create row
        if (! rows[y]) {
            rows[y] = {
                id: worksheet.rows[y].id,
                row: y,
                data: {},
            }
        }
        // Key
        let key = worksheet.options.columns[x].name;
        // Data
        rows[y].data[key] = data[i].value;
    }

    // Filter rows
    return rows.filter(function(row) {
        return row != null;
    });
}

export default function App() {
    const spreadsheet = useRef();

    // Data
    const data = [
        {
            "title": "Unlocking Android",
            "pageCount": 416,
            "publishedDate": "2009-04-01" ,
            "authors": "W. Frank Ableson, Charlie Collins, Robi Sen",
            "categories": "Open Source;Mobile"
        },
        {
            "title": "Android in Action, Second Edition",
            "pageCount": 592,
            "publishedDate": "2011-01-14",
            "authors": "W. Frank Ableson, Robi Sen",
            "categories": "Java"
        },
        {
            "title": "Flex 3 in Action",
            "pageCount": 576,
            "publishedDate": "2009-02-02",
            "authors": "Tariq Ahmed with Jon Hirschi, Faisal Abid",
            "categories": "Internet"
        },
    ];

    // Rows with IDs
    const rows = [
        { id: '4ab2b86b-6b48-4234-82c9-ea614374fb0b' },
        { id: 'ce9fbb3a-2330-425d-9a81-e75b711bec8d' },
        { id: 'd57533c3-d181-4083-b999-b8bc58bbf236' }
    ];

    // Columns
    const columns = [
        {
            type: 'text',
            title: 'Title',
            name: 'title',
            width: '150px',
        },
        {
            type: 'text',
            width: '55px',
            title: 'Pages',
            name: 'pageCount'
        },
        {
            type: 'calendar',
            width: '90px',
            title: 'Published',
            name: 'publishedDate'
        },
        {
            type: 'text',
            title: 'Author',
            name: 'authors',
            width: '150px',
        },
        {
            type: 'dropdown',
            title: 'Categories',
            name: 'categories',
            source: [
                'Internet',
                'Web Development',
                'Java',
                'Mobile',
                'Open Source'
            ],
            width: '200px',
            render: 'tag',
            multiple: true
        },
    ];

    // Persistence function
    const persistence = function(worksheet, method, payload) {
        let ignore = ['setBorder', 'resetBorders' ];
        // Ignore the methods to update the borders
        if (ignore.indexOf(method) >= 0) {
            return false;
        } else {
            // Aggregate rows for the following methods
            if (method === 'setValue' || method === 'setFormula') {
                payload = aggregateRows(worksheet, payload.data)
            }
            // Create attributes to be sent
            let formData = new FormData();
            formData.append('data', JSON.stringify(payload));
            // Fetch options
            const fetchOptions = {
                method: 'POST',
                headers: {
                    'X-Requested-With': 'http',
                    'Authorization': 'Bearer your-token',
                },
                body: formData,
            };
            // Perform the fetch
            fetch('/save', fetchOptions).then(response => response.json()).then(function (result) {
                // Save your data remotely
            }).catch(function (result) {
                // Something went wrong
            });
        }
    }

    // Render react component
    return (
        <Spreadsheet ref={spreadsheet} persistence={persistence}>
            <Worksheet autoId={true} data={data} rows={rows} columns={columns} />
        </Spreadsheet>
    );
}
<template>
    <Spreadsheet ref="spreadsheet" :persistence="persistence">
        <Worksheet :autoId="true" :data="data" :rows="rows" :columns="columns" />
    </Spreadsheet>
</template>

<script>
import { Spreadsheet, Worksheet } from "@jspreadsheet/vue";
import "jsuites/dist/jsuites.css";
import "jspreadsheet/dist/jspreadsheet.css";

// You can use the following license for quick testing on localhost, StackBlitz, or CodeSandbox.
// The license is valid for one day, after which the spreadsheet will become read-only.
// For a longer trial period, you can create a free account and generate a demo license with an extended expiration date.
jspreadsheet.setLicense('NzdkNzc0ZjhlN2Q1YjRhZWI4ZWM1N2U4ODdiYzE1Y2UzMjZkZWNiMTdmZjU5NzI1MTM0N2NkY2Y5MDhhZWZlYTc2ZGRiY2JlMTg0NWZkYjc5MTRiNzE4MjI4ZDA0ZjI4YjRmMGMwNWQ1ZmFlNzhjMDI3YjBhODBhZDYzZWMyNTgsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94Tnprd01qRXlPRE15TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=');

/**
 * Aggregate all the changes in the same row
 */
const aggregateRows = function(worksheet, data) {
    let rows = [];
    for (let i = 0; i < data.length; i++) {
        // Coords
        let x = data[i].x;
        let y = data[i].y;
        // Create row
        if (! rows[y]) {
            rows[y] = {
                id: worksheet.rows[y].id,
                row: y,
                data: {},
            }
        }
        // Key
        let key = worksheet.options.columns[x].name;
        // Data
        rows[y].data[key] = data[i].value;
    }

    // Filter rows
    return rows.filter(function(row) {
        return row != null;
    });
}

export default {
    components: {
        Spreadsheet,
        Worksheet,
    },
    methods: {
        // Persistence function
        persistence(worksheet, method, payload) {
            let ignore = ['setBorder', 'resetBorders' ];
            // Ignore the methods to update the borders
            if (ignore.indexOf(method) >= 0) {
                return false;
            } else {
                // Aggregate rows for the following methods
                if (method === 'setValue' || method === 'setFormula') {
                    payload = aggregateRows(worksheet, payload.data)
                }
                // Create attributes to be sent
                let formData = new FormData();
                formData.append('data', JSON.stringify(payload));
                // Fetch options
                const fetchOptions = {
                    method: 'POST',
                    headers: {
                        'X-Requested-With': 'http',
                        'Authorization': 'Bearer your-token',
                    },
                    body: formData,
                };
                // Perform the fetch
                fetch('/save', fetchOptions).then(response => response.json()).then(function (result) {
                    // Save your data remotely
                }).catch(function (result) {
                    // Something went wrong
                });
            }
        }
    },
    data() {
        // Data
        const data = [
            {
                "title": "Unlocking Android",
                "pageCount": 416,
                "publishedDate": "2009-04-01" ,
                "authors": "W. Frank Ableson, Charlie Collins, Robi Sen",
                "categories": "Open Source;Mobile"
            },
            {
                "title": "Android in Action, Second Edition",
                "pageCount": 592,
                "publishedDate": "2011-01-14",
                "authors": "W. Frank Ableson, Robi Sen",
                "categories": "Java"
            },
            {
                "title": "Flex 3 in Action",
                "pageCount": 576,
                "publishedDate": "2009-02-02",
                "authors": "Tariq Ahmed with Jon Hirschi, Faisal Abid",
                "categories": "Internet"
            },
        ];

        // Rows with IDs
        const rows = [
            { id: '4ab2b86b-6b48-4234-82c9-ea614374fb0b' },
            { id: 'ce9fbb3a-2330-425d-9a81-e75b711bec8d' },
            { id: 'd57533c3-d181-4083-b999-b8bc58bbf236' }
        ];

        // Columns
        const columns = [
            {
                type: 'text',
                title: 'Title',
                name: 'title',
                width: '150px',
            },
            {
                type: 'text',
                width: '55px',
                title: 'Pages',
                name: 'pageCount'
            },
            {
                type: 'calendar',
                width: '90px',
                title: 'Published',
                name: 'publishedDate'
            },
            {
                type: 'text',
                title: 'Author',
                name: 'authors',
                width: '150px',
            },
            {
                type: 'dropdown',
                title: 'Categories',
                name: 'categories',
                source: [
                    'Internet',
                    'Web Development',
                    'Java',
                    'Mobile',
                    'Open Source'
                ],
                width: '200px',
                render: 'tag',
                multiple: true
            },
        ];

        return {
            data,
            rows,
            columns,
        };
    }
}
</script>
import { Component, ViewChild, ElementRef } from "@angular/core";
import jspreadsheet from "jspreadsheet";
import "jspreadsheet/dist/jspreadsheet.css"
import "jsuites/dist/jsuites.css"

// You can use the following license for quick testing on localhost, StackBlitz, or CodeSandbox.
// The license is valid for one day, after which the spreadsheet will become read-only.
// For a longer trial period, you can create a free account and generate a demo license with an extended expiration date.
jspreadsheet.setLicense('NzdkNzc0ZjhlN2Q1YjRhZWI4ZWM1N2U4ODdiYzE1Y2UzMjZkZWNiMTdmZjU5NzI1MTM0N2NkY2Y5MDhhZWZlYTc2ZGRiY2JlMTg0NWZkYjc5MTRiNzE4MjI4ZDA0ZjI4YjRmMGMwNWQ1ZmFlNzhjMDI3YjBhODBhZDYzZWMyNTgsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94Tnprd01qRXlPRE15TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=');

/**
 * Aggregate all the changes in the same row
 */
const aggregateRows = function(worksheet, data) {
    let rows = [];
    for (let i = 0; i < data.length; i++) {
        // Coords
        let x = data[i].x;
        let y = data[i].y;
        // Create row
        if (! rows[y]) {
            rows[y] = {
                id: worksheet.rows[y].id,
                row: y,
                data: {},
            }
        }
        // Key
        let key = worksheet.options.columns[x].name;
        // Data
        rows[y].data[key] = data[i].value;
    }

    // Filter rows
    return rows.filter(function(row) {
        return row != null;
    });
}

@Component({
    standalone: true,
    selector: "app-root",
    template: `<div #spreadsheet></div>`,
})
export class AppComponent {
    @ViewChild("spreadsheet") spreadsheet: ElementRef;
    // Worksheets
    worksheets: jspreadsheet.worksheetInstance[];
    // Create a new data grid
    ngAfterViewInit() {
        // Create spreadsheet
        this.worksheets = jspreadsheet(this.spreadsheet.nativeElement, {
            worksheets: [
                {
                    autoId: true,
                    data: [
                        {
                            "title": "Unlocking Android",
                            "pageCount": 416,
                            "publishedDate": "2009-04-01" ,
                            "authors": "W. Frank Ableson, Charlie Collins, Robi Sen",
                            "categories": "Open Source;Mobile"
                        },
                        {
                            "title": "Android in Action, Second Edition",
                            "pageCount": 592,
                            "publishedDate": "2011-01-14",
                            "authors": "W. Frank Ableson, Robi Sen",
                            "categories": "Java"
                        },
                        {
                            "title": "Flex 3 in Action",
                            "pageCount": 576,
                            "publishedDate": "2009-02-02",
                            "authors": "Tariq Ahmed with Jon Hirschi, Faisal Abid",
                            "categories": "Internet"
                        },
                    ],
                    columns: [
                        {
                            type: 'text',
                            title: 'Title',
                            name: 'title',
                            width: '150px',
                        },
                        {
                            type: 'text',
                            width: '55px',
                            title: 'Pages',
                            name: 'pageCount'
                        },
                        {
                            type: 'calendar',
                            width: '90px',
                            title: 'Published',
                            name: 'publishedDate'
                        },
                        {
                            type: 'text',
                            title: 'Author',
                            name: 'authors',
                            width: '150px',
                        },
                        {
                            type: 'dropdown',
                            title: 'Categories',
                            name: 'categories',
                            source: [
                                'Internet',
                                'Web Development',
                                'Java',
                                'Mobile',
                                'Open Source'
                            ],
                            width: '200px',
                            render: 'tag',
                            multiple: true
                        },
                    ],
                    rows: [
                        { id: '4ab2b86b-6b48-4234-82c9-ea614374fb0b' },
                        { id: 'ce9fbb3a-2330-425d-9a81-e75b711bec8d' },
                        { id: 'd57533c3-d181-4083-b999-b8bc58bbf236' }
                    ]
                }
            ],
            persistence: function(worksheet, method, payload) {
                let ignore = ['setBorder', 'resetBorders' ];
                // Ignore the methods to update the borders
                if (ignore.indexOf(method) >= 0) {
                    return false;
                } else {
                    // Aggregate rows for the following methods
                    if (method === 'setValue' || method === 'setFormula') {
                        payload = aggregateRows(worksheet, payload.data)
                    }
                    // Create attributes to be sent
                    let formData = new FormData();
                    formData.append('data', JSON.stringify(payload));
                    // Fetch options
                    const fetchOptions = {
                        method: 'POST',
                        headers: {
                            'X-Requested-With': 'http',
                            'Authorization': 'Bearer your-token',
                        },
                        body: formData,
                    };
                    // Perform the fetch
                    fetch('/save', fetchOptions).then(response => response.json()).then(function (result) {
                        // Save your data remotely
                    }).catch(function (result) {
                        // Something went wrong
                    });
                }
            }
        });
    }
}

Custom Row IDs

The following example demonstrates custom ID generation with action buttons embedded in the last column.

<html>
<script src="https://jspreadsheet.com/v13/jspreadsheet.js"></script>
<script src="https://jsuites.net/v6/jsuites.js"></script>
<link rel="stylesheet" href="https://jspreadsheet.com/v13/jspreadsheet.css" type="text/css" />
<link rel="stylesheet" href="https://jsuites.net/v6/jsuites.css" type="text/css" />

<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Material+Icons" />

<div id="spreadsheet"></div>

<script>
const action = function() {
    let methods = {};
 
    methods.createCell = function(cell, value, x, y, instance, options) {
        let input = document.createElement('i');
        input.className = 'material-icons';
        input.style.cursor = 'pointer';
        input.style.fontSize = '22px';
        input.innerHTML = "search";
        input.onclick = function() {
            let id = instance.getRowId(y);
            // Do some action
            alert(id);
        }
 
        cell.appendChild(input);

        // Readonly
        cell.classList.add('readonly');
    }
 
    return methods;
}();

// New random GUIDs
const guid = function() {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
        let r = Math.random() * 16 | 0;
        let v = c == 'x' ? r : (r & 0x3 | 0x8);
        return v.toString(16);
    });
}
 
jspreadsheet(document.getElementById('spreadsheet'), {
    worksheets: [
        {
            data: [
                ['Google', '5', ''],
                ['Bing', '4', ''],
                ['Yahoo', '1', ''],
                ['Duckduckgo', '5', ''],
            ],
            columns: [
                { type: 'text', width:'400px' },
                { type: 'rating', width:'100px' },
                { type: action, width:'100px' },
            ],
            rows: [
                { id: 'cae4673d-5c11-4ece-beb0-4d89fad5510c' },
                { id: '0bb4800f-c997-4f3c-b1aa-303bc49096df' },
                { id: '52694185-722d-47cf-b35e-ffa6323ddcef' },
                { id: '370915fe-0ce0-464a-915a-cdcf1c157b20' }, 
            ]
        }
    ],
    onbeforeinsertrow: function(worksheet, rows) {
        // Generate new random GUIDs for the new rows
        return rows.map(function(v) {
            return { ...v, id: guid() };
        });
    }
});
</script>
</html>
import React, { useRef } from "react";
import { Spreadsheet, Worksheet } from "@jspreadsheet/react";
import "jsuites/dist/jsuites.css";
import "jspreadsheet/dist/jspreadsheet.css";

// You can use the following license for quick testing on localhost, StackBlitz, or CodeSandbox.
// The license is valid for one day, after which the spreadsheet will become read-only.
// For a longer trial period, you can create a free account and generate a demo license with an extended expiration date.
jspreadsheet.setLicense('NzdkNzc0ZjhlN2Q1YjRhZWI4ZWM1N2U4ODdiYzE1Y2UzMjZkZWNiMTdmZjU5NzI1MTM0N2NkY2Y5MDhhZWZlYTc2ZGRiY2JlMTg0NWZkYjc5MTRiNzE4MjI4ZDA0ZjI4YjRmMGMwNWQ1ZmFlNzhjMDI3YjBhODBhZDYzZWMyNTgsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94Tnprd01qRXlPRE15TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=');

const action = function() {
    const methods = {};

    methods.createCell = function(cell, value, x, y, instance, options) {
        let input = document.createElement('i');
        input.className = 'material-icons';
        input.style.cursor = 'pointer';
        input.style.fontSize = '22px';
        input.innerHTML = "search";
        input.onclick = function() {
            let id = instance.getRowId(y);
            // Do some action
            alert(id);
        }

        cell.appendChild(input);

        // Readonly
        cell.classList.add('readonly');
    }

    return methods;
}();

// New random GUIDs
const guid = function() {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
        let r = Math.random() * 16 | 0;
        let v = c == 'x' ? r : (r & 0x3 | 0x8);
        return v.toString(16);
    });
}

export default function App() {
    const spreadsheet = useRef();

    // Data
    const data = [
        ['Google', '5', ''],
        ['Bing', '4', ''],
        ['Yahoo', '1', ''],
        ['Duckduckgo', '5', ''],
    ];

    // Rows with IDs
    const rows = [
        { id: 'cae4673d-5c11-4ece-beb0-4d89fad5510c' },
        { id: '0bb4800f-c997-4f3c-b1aa-303bc49096df' },
        { id: '52694185-722d-47cf-b35e-ffa6323ddcef' },
        { id: '370915fe-0ce0-464a-915a-cdcf1c157b20' },
    ];

    // Event handler for before insert row
    const onbeforeinsertrow = function(worksheet, rows) {
        // Generate new random GUIDs for the new rows
        return rows.map(function(v) {
            return { ...v, id: guid() };
        });
    }

    // Columns
    const columns = [
        { type: 'text', width:'400px' },
        { type: 'rating', width:'100px' },
        { type: action, width:'100px' },
    ];

    // Render react component
    return (
        <Spreadsheet ref={spreadsheet} onbeforeinsertrow={onbeforeinsertrow}>
            <Worksheet data={data} rows={rows} columns={columns} />
        </Spreadsheet>
    );
}
<template>
    <Spreadsheet ref="spreadsheet" :onbeforeinsertrow="onbeforeinsertrow">
        <Worksheet :data="data" :rows="rows" :columns="columns" />
    </Spreadsheet>
</template>

<script>
import { Spreadsheet, Worksheet } from "@jspreadsheet/vue";
import "jsuites/dist/jsuites.css";
import "jspreadsheet/dist/jspreadsheet.css";

// You can use the following license for quick testing on localhost, StackBlitz, or CodeSandbox.
// The license is valid for one day, after which the spreadsheet will become read-only.
// For a longer trial period, you can create a free account and generate a demo license with an extended expiration date.
jspreadsheet.setLicense('NzdkNzc0ZjhlN2Q1YjRhZWI4ZWM1N2U4ODdiYzE1Y2UzMjZkZWNiMTdmZjU5NzI1MTM0N2NkY2Y5MDhhZWZlYTc2ZGRiY2JlMTg0NWZkYjc5MTRiNzE4MjI4ZDA0ZjI4YjRmMGMwNWQ1ZmFlNzhjMDI3YjBhODBhZDYzZWMyNTgsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94Tnprd01qRXlPRE15TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=');

const action = function() {
    const methods = {};

    methods.createCell = function(cell, value, x, y, instance, options) {
        let input = document.createElement('i');
        input.className = 'material-icons';
        input.style.cursor = 'pointer';
        input.style.fontSize = '22px';
        input.innerHTML = "search";
        input.onclick = function() {
            let id = instance.getRowId(y);
            // Do some action
            alert(id);
        }

        cell.appendChild(input);

        // Readonly
        cell.classList.add('readonly');
    }

    return methods;
}();

// New random GUIDs
const guid = function() {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
        let r = Math.random() * 16 | 0;
        let v = c == 'x' ? r : (r & 0x3 | 0x8);
        return v.toString(16);
    });
}

export default {
    components: {
        Spreadsheet,
        Worksheet,
    },
    methods: {
        // Event handler for before insert row
        onbeforeinsertrow(worksheet, rows) {
            // Generate new random GUIDs for the new rows
            return rows.map(function(v) {
                return { ...v, id: guid() };
            });
        }
    },
    data() {
        // Data
        const data = [
            ['Google', '5', ''],
            ['Bing', '4', ''],
            ['Yahoo', '1', ''],
            ['Duckduckgo', '5', ''],
        ];

        // Rows with IDs
        const rows = [
            { id: 'cae4673d-5c11-4ece-beb0-4d89fad5510c' },
            { id: '0bb4800f-c997-4f3c-b1aa-303bc49096df' },
            { id: '52694185-722d-47cf-b35e-ffa6323ddcef' },
            { id: '370915fe-0ce0-464a-915a-cdcf1c157b20' },
        ];

        // Columns
        const columns = [
            { type: 'text', width:'400px' },
            { type: 'rating', width:'100px' },
            { type: action, width:'100px' },
        ];

        return {
            data,
            rows,
            columns,
        };
    }
}
</script>
import { Component, ViewChild, ElementRef } from "@angular/core";
import jspreadsheet from "jspreadsheet";

import "jspreadsheet/dist/jspreadsheet.css"
import "jsuites/dist/jsuites.css"

// You can use the following license for quick testing on localhost, StackBlitz, or CodeSandbox.
// The license is valid for one day, after which the spreadsheet will become read-only.
// For a longer trial period, you can create a free account and generate a demo license with an extended expiration date.
jspreadsheet.setLicense('NzdkNzc0ZjhlN2Q1YjRhZWI4ZWM1N2U4ODdiYzE1Y2UzMjZkZWNiMTdmZjU5NzI1MTM0N2NkY2Y5MDhhZWZlYTc2ZGRiY2JlMTg0NWZkYjc5MTRiNzE4MjI4ZDA0ZjI4YjRmMGMwNWQ1ZmFlNzhjMDI3YjBhODBhZDYzZWMyNTgsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94Tnprd01qRXlPRE15TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=');

const action = function() {
    let methods = {};

    methods.createCell = (cell, value, x, y, instance, options) => {
        let input = document.createElement('i');
        input.className = 'material-icons';
        input.style.cursor = 'pointer';
        input.style.fontSize = '22px';
        input.innerHTML = "search";
        input.onclick = function() {
            let id = instance.getRowId(y);
            // Do some action
            alert(id);
        }

        cell.appendChild(input);

        // Readonly
        cell.classList.add('readonly');
    }

    return methods;
}();

// New random GUIDs
const guid = function() {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
        let r = Math.random() * 16 | 0;
        let v = c == 'x' ? r : (r & 0x3 | 0x8);
        return v.toString(16);
    });
}

@Component({
    standalone: true,
    selector: "app-root",
    template: `<div #spreadsheet></div>`,
})
export class AppComponent {
    @ViewChild("spreadsheet") spreadsheet: ElementRef;
    // Worksheets
    worksheets: jspreadsheet.worksheetInstance[];
    // Create a new data grid
    ngAfterViewInit() {
        // Create spreadsheet
        this.worksheets = jspreadsheet(this.spreadsheet.nativeElement, {
            worksheets: [{
                data: [
                    ['Google', '5', ''],
                    ['Bing', '4', ''],
                    ['Yahoo', '1', ''],
                    ['Duckduckgo', '5', ''],
                ],
                columns: [
                    { type: 'text', width:'400px' },
                    { type: 'rating', width:'100px' },
                    { type: action, width:'100px' },
                ],
                rows: [
                    { id: 'cae4673d-5c11-4ece-beb0-4d89fad5510c' },
                    { id: '0bb4800f-c997-4f3c-b1aa-303bc49096df' },
                    { id: '52694185-722d-47cf-b35e-ffa6323ddcef' },
                    { id: '370915fe-0ce0-464a-915a-cdcf1c157b20' },
                ],
            }],
            onbeforeinsertrow: function(worksheet, rows) {
                // Generate new random GUIDs for the new rows
                return rows.map(function(v) {
                    return { ...v, id: guid() };
                });
            }
        });
    }
}