npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

s8s-gtable

v0.1.4

Published

Google Spreadsheet rendered as HTML Table by React

Downloads

7

Readme

Overview

This package creates a React-rendering of a Google Spreadsheet into HTML Table. You will need to write your own back-end / api integration to make this work. See Implementation for more details there.

Google Sheet GTable Updated Inputs

Settings

To save myself some writing, I've copied/pasted the documentation from the /src/js/GTableContext.ts file:

export class GTableSettings {
    /**
     * Set this to true to chane to "column-major" tabbing through inputs.
     * TRUE: Using the TAB key will go down a column, then to the next one.
     * FALSE: Using the TAB key will go across a row, then to the next one.
     */
    columnTabbing = false;

    /**
     * An array of functions to add to the tables behavior (added with React.useEffect).
     * This allows for conditionally enabling tooltips
     * (and consequently removing the bootstrap dependency from this project)
     */
    effects: React.EffectCallback[] = [];
    
    /**
     * This function allows you define _what_ criteria makes a cell editable.
     * Trivially, you can always return false (default) for read-only,
     * or return true for a fully editable spreadsheet.
     */
    configurable: ((cellData?: google.CellData) => boolean) = (() => { return false; });
    
    /**
     * When the end-user presses enter an array of updated values (from configurable cells)
     * will be passed to this handler.
     * These values can be passed straight to the Google Sheets API as is.
     * The handler needs to optionally accept an array of objects with two properties:
     *   - range: the address of the updated cell, e.g. A1
     *   - values: A 2D array of the value of updated cell, e.g. [[0]]
     */
    submitHandler: ((data?:GTableInputData[]) => void) = (() => { return; });
    
    /**
     * If set to true, calls to GTable.renderGTable will add properties for
     *   - numrows: the number of rows in the spreadsheet data
     *   - numcols: the number of columns in the spreadsheet data
     *   - data: a 2D array of values from the spreadsheet
     */
    createmaxtrix = false;
};

Implementation

You will need to have created a Google OAuth2 client to access the Sheets API. I will omit these steps here and assume you've done so. Using the backend from the S8S project (not yet open source) as an example for how to retrieve data from a google sheet.

Some notes:

  • Do not use the fields setting to create a partial response as GTable uses nearly every returned resource).
  • The below code snippets are incomplete, but sufficient for reference. (With the exception of the leftToTheReader() function to define the access token it might be complete but it has not been tested.)

Front-end


import { Tooltip } from 'bootstrap';
import type * as google from '../../shared/s8s-google-types';
import { renderGTable, GTableSettings } from 's8s-gtable';

function getTable() {
    let settings = {
        method: 'GET',
        headers: {
            "Accept": "application/json",
            "Content-Type": "application/json"
        }
    };

    fetch('/route/to/table', settings).then((resp) => {
        return resp.json();
    }).then((data) => {
        drawTable('data-table-container', data);
    });

}
getTable();

function drawTable(tbl: string, data: google.GoogleSpreadsheet) {
    let settings = new GTableSettings();
    settings.columnTabbing = true; // S8S tables have column-oriented input "forms"

    // S8S tables use boostrap tooltips
    settings.effects = [
        () => {
            var ttTriggerList = Array.prototype.slice.call(
                document.querySelectorAll('[data-toggle="tooltip"]')
            );
            /*var tooltipList =*/ ttTriggerList.map(function (ttTriggerEl) {
                return new Tooltip(ttTriggerEl)
            });
        }
    ];

    // S8S uses yellow backgrounds to denote user-editable cell values
    settings.configurable = (data?: google.CellData) => {
        if (!data) { return false; }
        let bgStyle = data.effectiveFormat?.backgroundColorStyle?.rgbColor;
        let bgColor = data.effectiveFormat?.backgroundColor;
        return (bgColor?.red == 1 && bgColor?.green == 1 && (bgColor?.blue || 0) == 0)
            || (bgStyle?.red == 1 && bgStyle?.green == 1 && (bgStyle?.blue || 0) == 0);
    };

    renderGTable(tbl, data, settings);
}

Back-end (Node.js)


import https from 'https';
import type {Express, Request, Response} from 'express';
// The below should import the types from the src directory of this repo
import type * as google from '../../shared/s8s-google-types';

export class SheetSettings {
    accessToken:string = '';
    sheetId:string = '';
    range:string|string[] = [];
};
export class GetSheetSettings extends SheetSettings {
    fields?:string;
};
export class SetSheetSettings extends SheetSettings {
    /* TODO: document why this paramater exists */
    rangevalues?:any[];
    values?:any[];
};

export default function(app:Express) {

    app.get('/route/to/table', (req:Request, res:Response) => {
        getData(req, res);
    });

    // This route will become relevent later
    app.post('/route/to/table', jsonParser, (req:Request, res:Response) => {
        sendData(req, res);
    });
}

function getData(req:Request, res:Response) {
    let gSheet = 'some-sheet-id';
    
    let settings = new GetSheetSettings();
    settings.accessToken = leftToTheReader(); // This function must be written
    settings.sheetId = gSheet;
    settings.range = 'A1:D100'; // Can also use named ranges which I HIGHLY recommend

    getSheetData(settings).then((gMetaData:google.GoogleSpreadsheet) => {
        res.set('Content-Type', 'application/json');
        res.send(gMetaData);
    }).catch((err) => {
        // Left to the reader
    });
}

function getSheetData(settings:GetSheetSettings):Promise<google.TableData> {
    let rangeQuery = '&ranges=';
    if (Array.isArray(settings.range)) {
        rangeQuery = settings.range.join('&ranges=');
    } else {
        rangeQuery += settings.range;
    }

    let fieldsQuery = '';
    if (settings.fields != undefined) {
        fieldsQuery = '&fields=' + settings.fields;
    }
    var options = {
        method: 'GET',
        host: 'sheets.googleapis.com',
        path: '/v4/spreadsheets/' + settings.sheetId + '?includeGridData=true' + rangeQuery + fieldsQuery,
        headers: {
            'Authorization': 'Bearer ' + settings.accessToken
        }
    };

    return new Promise<google.TableData>((resolve, reject) => {
        const req = https.request(options, (res) => {
            console.log(`${options.host}${options.path} : ${res.statusCode} (${res.statusMessage})`);
            var datastr = '';
            res.on('data', (data) => {
                datastr += data;
            });
            res.on('end', () => {
                let data = JSON.parse(datastr);

                if (isSuccess(data)) {
                    resolve(data); // <-- This object will have a Spreadsheet data type
                                   //     (found in src/types/google-types.d.ts)
                } else {
                    reject(data.error); // <-- This object will have a GoogleAPIError data type
                                        //     (found in src/types/s8s-google-types.d.ts)
                }
            });
        });
        req.on('error', (err:types.GoogleError) => { reject(err); });
        req.end();
    });
};

Handling Updates

You can handle updates to your configurable cells by providing a settings.submitHandler value. An example implementation follows.

Add the following to wherever you created your GTableSettings.

// Put this wherever you created your settings object:
settings.submitHandler = (data?:GTableInputData[]) => {
    if (!data) { return; }
    let settings = {
        method: 'POST',
        credentials: "include" as RequestCredentials,
        body: JSON.stringify(data),
        headers: {
            "Content-Type": "application/json"
        }
    };
    fetch('/forecaster/update', settings).then((_resp:Response) => {
        getData();
    });
};

Update your back-end implementation with the below:


function sendData(req:Request, res:Response) {
    let gSheet = getSheetId(req);
    
    let settings = new google.SetSheetSettings();
    settings.accessToken = leftToTheReader(); // This function must be written
    settings.sheetId = gSheet;
    settings.rangevalues = req.body;

    // TODO: update range + values so they needn't be defined by the front-end
    setSheetData(settings).then((_data) => {
        res.send('success');
    }).catch((err) => {
        // Left to the reader
    });
}

function setSheetData(settings:SetSheetSettings) {
    var options = {
        method: 'POST',
        host: 'sheets.googleapis.com',
        path: '/v4/spreadsheets/' + settings.sheetId + '/values:batchUpdate',
        headers: {
            'Authorization': 'Bearer ' + settings.accessToken,
            'Content-Type': 'application/json'
        }
    };
    let data = [];
    if (settings.rangevalues != undefined) {
        data = settings.rangevalues;
    } else {
        // This path is useful for updating one cell or contiguous range at a time
        data = [{
            range: settings.range,
            values: settings.values
        }];
    }

    let post_data = {
        valueInputOption: "USER_ENTERED",
        data: data,
        includeValuesInResponse: false
    };

    return new Promise((resolve, reject) => {
        const req = https.request(options, (res) => {
            console.log(`${options.host}${options.path} : ${res.statusCode}`);
            res.setEncoding('utf8');

            var datastr = '';
            res.on('data', (data) => {
                datastr += data;
            });
            res.on('end', () => {
                let data = JSON.parse(datastr);
                if (isSuccess(data)) {
                    resolve(data);
                } else {
                    console.log(`ERROR IN POST: ${post_data}`);
                    reject(data.error);
                }
            });
        });
        req.on('error', (err:GoogleError) => { errHandler(err, reject); });
        req.write(JSON.stringify(post_data));
        req.end();
    });
};