Skip to content

Instantly share code, notes, and snippets.

@alekrutkowski
Last active October 20, 2025 14:55
Show Gist options
  • Select an option

  • Save alekrutkowski/e8052bb1ffbc812a6540eee20249ac59 to your computer and use it in GitHub Desktop.

Select an option

Save alekrutkowski/e8052bb1ffbc812a6540eee20249ac59 to your computer and use it in GitHub Desktop.

Revisions

  1. alekrutkowski revised this gist Oct 20, 2025. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion index.md.txt
    Original file line number Diff line number Diff line change
    @@ -145,7 +145,7 @@ const input_text = view(
    ```

    </div>
    <div class="card grid-colspan-3">
    <div class="card grid-colspan-3">${rows ? "" : ""} <!-- to display the built-in Observable spinner rather than an empty block when waiting for user input -->

    ```js
    view(html`<table id="myTable" style="width:100%;"></table>`);
  2. alekrutkowski created this gist Oct 7, 2025.
    311 changes: 311 additions & 0 deletions index.md.txt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,311 @@
    ---
    toc: false
    theme: [dashboard]
    footer: ""
    ---

    <link href="https://unpkg.com/gridjs/dist/theme/mermaid.min.css" rel="stylesheet" />

    <script src="https://unpkg.com/gridjs/dist/gridjs.umd.js"></script>

    <div id="global-spinner" style="
    display: none;
    position: fixed;
    top: 20px;
    right: 20px;
    z-index: 9999;
    ">
    <div class="spinner"></div>
    </div>

    # My Dashboard

    ```js
    // Message displayed before the first Excel file upload
    // Reactively watch for a table being added

    // Create a message element
    const msg = document.createElement("div");
    msg.textContent = "Upload an Excel file, select color, pick a date.";
    msg.classList.add("info-box");
    document.querySelector("h1").insertAdjacentElement("afterend", msg);

    // NOT USED SINCE myTable IS IN view()
    // // Function to check for a table
    // function checkForTable() {
    // msg.remove(); // remove message once a table appears
    // observer.disconnect(); // stop watching
    // }

    // const target = document.getElementById("myTable");

    // // Create the observer
    // const observer = new MutationObserver(checkForTable);

    // // Start observing #myTable
    // observer.observe(target, {
    // childList: true, // detect added/removed rows/cells
    // subtree: true, // detect changes inside descendants
    // characterData: true, // detect text changes in cells
    // attributes: true // uncomment if you also want attribute changes (like style/class)
    // });

    import * as XLSX from "https://cdn.jsdelivr.net/npm/xlsx@0.18.5/+esm";
    import { WebR } from "https://webr.r-wasm.org/v0.4.4/webr.mjs";
    const webR = new WebR();
    await webR.init();
    await webR.installPackages(["openxlsx2"]);
    ```

    ```js
    const spinner = document.getElementById("global-spinner");
    spinner.style.display = "flex";

    // Read the uploaded file into a workbook
    const selected = await fileInput; // value of the view() is the selected File (or array if multiple)
    const file = Array.isArray(selected) ? selected[0] : selected;
    const arrayBuffer = await file.arrayBuffer();
    const uint8Array = new Uint8Array(arrayBuffer);

    // Load the uploaded file into WebR
    await webR.FS.writeFile("input.xlsx", uint8Array);

    // Run R script to process the file with openxlsx2
    const rCode = `
    library(openxlsx2)
    wb <- wb_load("/home/web_user/input.xlsx")
    wb <- wb_add_worksheet(wb, sheet="Summary", tab_color = wb_color("${input_radios_single_choice}"))
    wb <- wb_add_data(wb, sheet="Summary", x=data.frame(Info="Processed with WebR by ${input_text} on ${input_date}"))
    wb_save(wb, "/home/web_user/output.xlsx", overwrite = TRUE)
    `;
    console.log(rCode);
    await webR.evalR(rCode);

    // https://docs.r-wasm.org/webr/latest/convert-r-to-js.html#:~:text=converted%20into%20a-,D3%2Dstyle%20data%20array
    const df = await webR.evalR("wb_to_df(wb)");
    const rows = await df.toD3();

    // Obtain the contents of the file from the VFS
    const xlsxdata = await webR.FS.readFile("/home/web_user/output.xlsx");

    // Serialize to a Blob
    const outBlob = new Blob([xlsxdata], {
    type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
    });

    // Derive a filename like originalname-processed.xlsx
    const originalName = file?.name?.replace(/\.(xlsx|xls)$/i, "") ?? "data";
    const downloadName = `${originalName}-processed.xlsx`;

    spinner.style.display = "none";
    const element = document.querySelector(".info-box");
    if (element) {
    element.remove(); // removes it from the DOM
    }
    ```

    <div class="grid grid-cols-4" style="align-items: flex-start; grid-auto-rows: auto">
    <div class="card">

    ```js
    const input_switch = view(
    Inputs.toggle({ label: "Not really used", value: true })
    );
    ```

    ```js
    const input_radios_single_choice = view(
    Inputs.radio(["red", "green", "blue"], { label: "Color" , value: "red"})
    );
    ```

    ```js
    const input_date = view(Inputs.date({ label: "Date", required: true }));
    ```

    ```js
    const fileInput = view(
    Inputs.file({
    label: "Excel file",
    accept: ".xlsx",
    required: true,
    multiple: true,
    })
    );
    ```

    ```js
    const input_text = view(
    Inputs.text({
    label: "Name",
    placeholder: "Enter your name",
    value: "Anonymous",
    })
    );
    ```

    </div>
    <div class="card grid-colspan-3">

    ```js
    view(html`<table id="myTable" style="width:100%;"></table>`);
    new gridjs.Grid({
    columns: Object.keys(rows[0]).map((k) => ({
    id: k,
    name: k,
    sort: true,
    resizable: true,
    })),
    data: rows,
    resizable: true,
    search: true,
    sort: true,
    pagination: { enabled: true, limit: 10 },
    height: "600px",
    }).render(document.getElementById("myTable"));
    ```

    ```js
    // Download button
    const button = html`<button>&DownArrowBar; Download processed Excel</button>`;

    button.onclick = () => {
    const a = document.createElement("a");
    a.href = URL.createObjectURL(outBlob);
    a.download = downloadName;
    a.click();
    URL.revokeObjectURL(a.href);
    };

    display(button);
    ```

    </div>

    </div>

    <style>

    /* --- Grid.js table compact + resizable + wrap headers --- */

    /* Table layout tweaks */
    .gridjs-table {
    width: 100%;
    table-layout: fixed; /* keeps columns stable when resizing */
    }

    /* Headers: allow wrapping, prevent clipping */
    .gridjs-th {
    position: relative; /* needed for the resize handle */
    white-space: normal !important; /* let long names wrap */
    overflow: visible !important;
    text-overflow: unset !important;
    vertical-align: middle;
    }

    /* Body cells: normal overflow (so content can be seen or wrapped if you wish) */
    .gridjs-td {
    vertical-align: middle;
    white-space: normal; /* set to nowrap if you prefer */
    overflow: hidden; /* avoid spilling beyond cell */
    text-overflow: ellipsis; /* adjust to taste */
    }

    /* Compact, balanced padding (headers and cells) */
    .gridjs-th,
    .gridjs-td {
    padding: 6px 10px 5px 10px !important; /* top right bottom left */
    }

    /* Optional: a touch more padding for header row only */
    .gridjs-tr:nth-child(1) .gridjs-th {
    padding-top: 8px !important;
    padding-bottom: 6px !important;
    }

    /* Column min width so wrapped headers do not collapse too narrowly */
    .gridjs-th,
    .gridjs-td {
    min-width: 100px; /* adjust to your data */
    }

    /* Show full header content on hover if you keep ellipsis somewhere */
    .gridjs-th:hover .gridjs-th-content {
    overflow: visible;
    }

    /* Resize handle: thin right-edge strip, not a full overlay */
    .gridjs-th .gridjs-resizable {
    position: absolute !important;
    top: 0 !important;
    bottom: 0 !important;
    right: 0 !important;
    left: auto !important;
    width: 8px !important; /* thickness of the drag area */
    height: 100% !important;
    cursor: col-resize !important;
    background: transparent !important;
    z-index: 2;
    pointer-events: auto !important;
    }

    /* Keep header text and sort control above the cell background */
    .gridjs-th .gridjs-th-content,
    .gridjs-th .gridjs-sort {
    position: relative;
    z-index: 3;
    pointer-events: auto;
    }

    /* Optional: subtle visual cue when hovering near the edge */
    .gridjs-th .gridjs-resizable:hover {
    background: rgba(0, 0, 0, 0.06) !important;
    }

    /* Ensure the last column can also be resized, if desired */
    .gridjs-th:last-child .gridjs-resizable {
    display: block !important;
    }

    /* Optional: slightly thinner borders for a denser look */
    .gridjs-td,
    .gridjs-th {
    border-width: 2px;
    }

    /* Remove the blue/gray rectangle for column resize handles */
    .gridjs-th .gridjs-resizable,
    .gridjs-th .gridjs-resizable:hover,
    .gridjs-th .gridjs-resizable:active {
    background: transparent !important;
    }


    .info-box {
    display: inline-block; /* shrink to fit text */
    background-color: #f0f8ff;
    border: 1px solid #b3d4fc; /* thin border around */
    padding: 8px 12px;
    margin: 8px 0;
    border-radius: 8px;
    font-family: Arial, sans-serif;
    font-size: 12px;
    color: #333;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
    width: fit-content; /* adapts to text width */
    max-width: 80%; /* prevent over-expansion */
    }

    .spinner {
    width: 40px;
    height: 40px;
    border: 4px solid #ccc;
    border-top-color: #333;
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
    }

    @keyframes spin {
    to { transform: rotate(360deg); }
    }
    </style>