Tables (ListObject) and Comments — creation, totals row, structured references, memos
level: intermediate / verified: 2026-10-01 / tabs: Node.js・Python・Java・WebAssembly / 日本語
What you want to do
Turn a data range into an Excel "table" (ListObject) and work with its name, column names, style and totals row. Use "structured references" to refer to table columns from formulas, and add, read and remove cell comments (modern threaded comments) and memos (legacy notes).
Classes used
Get tables from ws.getListObjects() and comments from ws.getComments().
ListObjects (collection of tables)
| Method | Description |
|---|---|
addList(name, A1C1, useFirstRowAsHeader, insertTotals) | Creates a table from an existing cell range and returns the Table. |
addListFromRange(name, topA1, copyFromA1C1, useFirstRowAsHeader, insertTotals) | Creates a table by copying the data to topA1. copyFromA1C1 may point to another sheet, like Sheet2!A1:C1. |
getList(name) | Gets a table. Throws a table is not found exception if it does not exist. |
removeList(name, deleteData?) | Removes a table. deleteData defaults to true (the data is deleted too). Pass false to keep it. |
Table (a single table)
| Group | Methods |
|---|---|
| Ranges | getAllRange() / getHeaderRowRange() / getDataBodyRange() / getTotalRowRange() (all return a Range; getTotalRowRange() throws totalRow is not set when there is no totals row) |
| Name | getName() / changeTableName(name) |
| Column names | getColumnName() / setColumnName([...]) (also changes what the header cells display) |
| Style | getTableStyleName() / setBuiltinStyleName(enums.XlDefaultTableStyle.Xxx, clearFormat) / setCustomTableStyleName(name, clearFormat) |
| Totals row | isShowTotals() / setShowTotals(bool) / getTotalRowFunction(col) / setTotalRowFunction(col, enums.XlTotalsCalculation.Xxx) / setCustomRowFunction(col, formula, isArray?) / getTotalRowLabel(col) / setTotalRowLabel(col, text) |
| Filter / display | getAutoFilter() / removeAutoFilter() / isShowAutoFilter() / isShowHeaders() / setShowHeaders(bool) / isShowTableStyleRowStripes() and more |
Comments (cell comments)
Excel has two comment systems: modern comments (threads, replies, done flag) and legacy memos (callout notes).
| Method | System | Description |
|---|---|---|
setComment(A1, commentObject) | modern | Adds one comment. On a cell that already has comments this appends (automatically as a reply). |
setCommentThread(A1, [commentObject...]) | modern | Sets a whole thread (overwrites). The first entry is the body, the rest are replies. |
getComment(A1) | modern | Returns Array<dto.CommentObject>. Throws comment not found if there is none. |
removeComment(A1) | modern | Removes modern comments. |
setMemo(A1, author, text, fontObject, visible?, rowColumnsNum?, colColumnsNum?) | legacy | Sets a callout memo. fontObject can be new dto.FontObject(). |
getMemoText(A1) / getMemoAuthor(A1) | legacy | Gets the memo text / author. Throws memo not found if there is none. |
removeMemo(A1) | legacy | Removes a memo. |
dto.CommentObject has setAuthor / setContent / setParentId (parent ID of a reply) / setDone (done flag) plus the corresponding getters. Once set, an ID is automatically assigned as a UUID in {...} form.
Totals functions enums.XlTotalsCalculation
| Member | Value | Meaning |
|---|---|---|
TotalsCalculationNone | 0 | No totals |
TotalsCalculationSum | 1 | Sum |
TotalsCalculationAverage | 2 | Average |
TotalsCalculationCount | 3 | Count |
TotalsCalculationCountNums | 4 | Count of numbers |
TotalsCalculationMin / TotalsCalculationMax | 5 / 6 | Min / Max |
TotalsCalculationStdDev / TotalsCalculationVar | 7 / 8 | StdDev / Var |
TotalsCalculationCustom | 9 | Custom formula (set with setCustomRowFunction) |
Code
Output:
// tables-comments.js — table (ListObject) creation, totals row, structured references, cell comments
const { nodeosbxl, enums, dto } = require("nodeosbxl");
const app = new nodeosbxl.App();
const wb = app.createWorkBook("tables-comments.xlsx");
const ws = wb.openWorkSheet("Sheet1");
// ---------- fill in the data (1 header row + 3 data rows) ----------
const rows = [
["Product", "Unit Price", "Qty"],
["Apple", 120, 3],
["Orange", 80, 5],
["Grape", 300, 2],
];
const values = [];
rows.forEach((row, ri) => row.forEach((cell, ci) => {
const a1 = app.convertFromRowColNumber(ri + 1, ci + 1);
const o = new dto.InputValueObject();
if (typeof cell === "number") o.setNumberValue(a1, cell);
else o.setStringValue(a1, cell);
values.push(o);
}));
ws.setValueArray(values);
// ---------- 1. create the table ----------
const lo = ws.getListObjects();
const table = lo.addList("Sales", "A1:C4", true, true); // useFirstRowAsHeader, insertTotals
console.log("[1] Table created");
console.log(` name = ${table.getName()}`);
console.log(` columns = ${JSON.stringify(table.getColumnName())}`);
console.log(` style = ${table.getTableStyleName()}`);
console.log(` full range = ${table.getAllRange().getAddress()}`);
console.log(` header = ${table.getHeaderRowRange().getAddress()}`);
console.log(` data body = ${table.getDataBodyRange().getAddress()}`);
console.log(` totals row = ${table.getTotalRowRange().getAddress()}`);
// ---------- 2. totals row ----------
table.setTotalRowLabel(1, "Total");
table.setTotalRowFunction(2, enums.XlTotalsCalculation.TotalsCalculationSum);
table.setTotalRowFunction(3, enums.XlTotalsCalculation.TotalsCalculationSum);
console.log("[2] Totals row");
console.log(` label (col 1) = ${JSON.stringify(table.getTotalRowLabel(1))}`);
console.log(` formula = ${JSON.stringify(ws.getRange("B5:C5").getFormula())}`);
console.log(` value = ${JSON.stringify(ws.getRange("B5:C5").getValue(true))}`);
// ---------- 3. change the table style ----------
table.setBuiltinStyleName(enums.XlDefaultTableStyle.TableStyleMedium9, true);
console.log(`[3] style after change = ${table.getTableStyleName()}`);
// ---------- 4. structured reference (formula referring to a table column) ----------
ws.getRange("E1:E1").setFormula("SUM(Sales[Unit Price])"); // no leading "="
console.log("[4] Structured reference");
console.log(` E1 formula = ${JSON.stringify(ws.getRange("E1:E1").getFormula())}`);
console.log(` E1 value = ${JSON.stringify(ws.getRange("E1:E1").getValue(true))}`);
// ---------- 5. comments (modern) and memos (legacy) ----------
const cm = ws.getComments();
const mkComment = (author, content) => {
const c = new dto.CommentObject();
c.setAuthor(author);
c.setContent(content);
return c;
};
// modern comment (thread): body + reply = 2 entries -> B2 (kept in the file after save)
cm.setCommentThread("B2", [mkComment("alice", "Please check this number"), mkComment("bob", "Checked")]);
// single modern comment -> D2 (used to demonstrate removal)
cm.setComment("D2", mkComment("dave", "Comment to delete later"));
// legacy memo (callout) -> A2
cm.setMemo("A2", "Carol", "Top-priority product", new dto.FontObject());
const thread = cm.getComment("B2");
console.log("[5] Comments");
console.log(` B2 thread count = ${thread.length}`);
thread.forEach((c, i) => console.log(` [${i}] ${c.getAuthor()}: ${c.getContent()}${c.getParentId() ? " (reply)" : ""}`));
console.log(` D2 comment count = ${cm.getComment("D2").length}`);
console.log(` A2 memo = ${JSON.stringify(cm.getMemoText("A2"))} / author ${JSON.stringify(cm.getMemoAuthor("A2"))}`);
// removal: removeComment for modern, removeMemo for legacy (reading after removal throws "not found")
cm.removeComment("D2");
cm.removeMemo("A2");
const tryRead = (label, fn) => {
try { fn(); console.log(` after removal ${label}: no exception`); }
catch (e) { console.log(` after removal ${label}: exception ${e.message}`); }
};
tryRead("D2 comment", () => cm.getComment("D2"));
tryRead("A2 memo", () => cm.getMemoText("A2"));
console.log(` B2 is kept, so ${cm.getComment("B2").length} comments remain after save`);
const total = ws.getRange("B5:B5").getValue(true)["B5"];
const e1 = ws.getRange("E1:E1").getValue(true)["E1"];
console.log(`[Check] table=${table.getName()} total(Unit Price)=${total} structuredRef(E1)=${e1}`);
wb.save();
wb.close();
[1] Table created
name = Sales
columns = ["Product","Unit Price","Qty"]
style = TableStyleMedium2
full range = A1:C5
header = A1:C1
data body = A2:C4
totals row = A5:C5
[2] Totals row
label (col 1) = "Total"
formula = {"B5":"SUBTOTAL(109,Sales[Unit Price])","C5":"SUBTOTAL(109,Sales[Qty])"}
value = {"B5":"500","C5":"10"}
[3] style after change = TableStyleMedium9
[4] Structured reference
E1 formula = {"E1":"SUM(Sales[Unit Price])"}
E1 value = {"E1":"500"}
[5] Comments
B2 thread count = 2
[0] alice: Please check this number
[1] bob: Checked (reply)
D2 comment count = 1
A2 memo = "Top-priority product" / author "Carol"
after removal D2 comment: exception comment not found
after removal A2 memo: exception memo not found
B2 is kept, so 2 comments remain after save
[Check] table=Sales total(Unit Price)=500 structuredRef(E1)=500
Notes
- Tables are handled by name, not by range. From the return value of
addList()(aTable) or fromgetList("Sales")you can pull out the header, data body and totals row ranges asRangeobjects.
Because a totals row was inserted,getAllRange()isA1:C5(dataA1:C4+ totals row 5). - The totals row holds
SUBTOTALformulas. SettingsetTotalRowFunction(2, ...Sum)auto-generatesSUBTOTAL(109,Sales[Unit Price])in B5 (109 = sum, nothing ignored). Average maps toSUBTOTAL(101,...)and Count toSUBTOTAL(103,...)(measured). To put an arbitrary formula there, usesetCustomRowFunction(col, "MAX(C2:C4)"); in that casegetTotalRowFunction()returnsTotalsCalculationCustom(9). - Structured references are written as "TableName[ColumnName]".
SUM(Sales[Unit Price])evaluates to 500. A bare column name (SUM(Unit Price)) gives a#NAME?error.
Formulas do not take a leading=. - Renaming a column also updates structured references inside the table. When you change column names with
setColumnName([...]),Sales[OldName]in the totals row and in existing formulas is automatically replaced withSales[NewName](measured). - There are two comment systems. The modern one with threads/replies/done flag (
setComment,setCommentThread,getComment) and the legacy Excel callout memo (setMemo,getMemoText). Pick whichever fits the use case.
Both are persisted to the file bysave().
Caveats
- Table names that look like cell references are not allowed.
addList("A1", ...)oraddList("S2", ...)throws atable name is invalid formatexception.
Re-creating a table with an existing name throwstable name is already exists. - With
addList(insertTotals=true)the default totals are "None" for every column (Excel-compatible, fixed 2026-10-01). Only the hardcoded label集計("Total" in Japanese) is placed automatically in the first column — set your own withsetTotalRowLabel(). No totals function is set for the data columns (older versions defaulted the last column toCount(SUBTOTAL(103))). Set the columns you want totaled explicitly withsetTotalRowFunction(). removeList()deletes the data by default.removeList("Sales")behaves asdeleteData=trueand wipes the cell values too.
To remove only the table outline and keep the values, useremoveList("Sales", false).- Modern comments require a
dto.CommentObjectinstance. A plain literal likesetComment("A1", { text: "..." })from the docs'@examplethrowsInvalid argument(measured). Build the object withnew dto.CommentObject()→setAuthor()/setContent()and pass that. setCommentappends,setCommentThreadoverwrites. CallingsetCommenton a cell that already has comments increases the count, and the new one automatically becomes a reply (withparentId) in the existing thread.
UsesetCommentThreadwhen you want to replace the whole thread (measured).- Reading a nonexistent comment/memo throws.
getComment()throwscomment not foundandgetMemoText()/getMemoAuthor()throwmemo not found(fixed 2026-10-01). Verify state after removal by catching the exception in a try/catch. - Averages etc. in the totals row come back as raw floating point.
getValue(true)returns pre-rounding values as strings, so an averageSUBTOTAL(101,...)can be something like3.3333333333333335(round it via the display format).
See also
- Index
- Previous: Bulk input of large data
- Building a table with values and formulas / Working with rows, columns and cell ranges
- API reference