@niicojs/excel 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,6 +10,8 @@ A TypeScript library for Excel/OpenXML manipulation with maximum format preserva
10
10
  - Cell styles (fonts, fills, borders, alignment)
11
11
  - Merged cells
12
12
  - Sheet operations (add, delete, rename, copy)
13
+ - Create sheets from arrays of objects (`addSheetFromData`)
14
+ - Convert sheets to JSON arrays (`toJson`)
13
15
  - Create new workbooks from scratch
14
16
  - TypeScript-first with full type definitions
15
17
 
@@ -28,6 +30,7 @@ import { Workbook } from '@niicojs/excel';
28
30
 
29
31
  // Create a new workbook
30
32
  const wb = Workbook.create();
33
+ wb.addSheet('Sheet1');
31
34
  const sheet = wb.sheet('Sheet1');
32
35
 
33
36
  // Write data
@@ -146,7 +149,7 @@ wb.addSheet('Data');
146
149
  wb.addSheet('Summary', 0); // Insert at index 0
147
150
 
148
151
  // Get sheet names
149
- console.log(wb.sheetNames); // ['Summary', 'Sheet1', 'Data']
152
+ console.log(wb.sheetNames); // ['Summary', 'Data']
150
153
 
151
154
  // Access sheets
152
155
  const sheet = wb.sheet('Data'); // By name
@@ -162,6 +165,115 @@ wb.copySheet('RawData', 'RawData_Backup');
162
165
  wb.deleteSheet('Summary');
163
166
  ```
164
167
 
168
+ ## Creating Sheets from Data
169
+
170
+ Create sheets directly from arrays of objects with `addSheetFromData`:
171
+
172
+ ```typescript
173
+ const wb = Workbook.create();
174
+
175
+ // Simple usage - object keys become column headers
176
+ const employees = [
177
+ { name: 'Alice', age: 30, city: 'Paris' },
178
+ { name: 'Bob', age: 25, city: 'London' },
179
+ ];
180
+
181
+ wb.addSheetFromData({
182
+ name: 'Employees',
183
+ data: employees,
184
+ });
185
+
186
+ // Custom column configuration
187
+ wb.addSheetFromData({
188
+ name: 'Custom',
189
+ data: employees,
190
+ columns: [
191
+ { key: 'name', header: 'Full Name' },
192
+ { key: 'age', header: 'Age (years)' },
193
+ { key: 'city', header: 'Location', style: { bold: true } },
194
+ ],
195
+ });
196
+
197
+ // With formulas and styles using RichCellValue
198
+ const orderLines = [
199
+ { product: 'Widget', price: 10, qty: 5, total: { formula: 'B2*C2', style: { bold: true } } },
200
+ { product: 'Gadget', price: 20, qty: 3, total: { formula: 'B3*C3', style: { bold: true } } },
201
+ ];
202
+
203
+ wb.addSheetFromData({
204
+ name: 'Orders',
205
+ data: orderLines,
206
+ });
207
+
208
+ // Other options
209
+ wb.addSheetFromData({
210
+ name: 'Options',
211
+ data: employees,
212
+ headerStyle: false, // Don't bold headers
213
+ startCell: 'B3', // Start at B3 instead of A1
214
+ });
215
+ ```
216
+
217
+ ## Converting Sheets to JSON
218
+
219
+ Convert sheet data back to arrays of objects with `toJson`:
220
+
221
+ ```typescript
222
+ const sheet = wb.sheet('Data');
223
+
224
+ // Using first row as headers
225
+ const data = sheet.toJson();
226
+ // [{ name: 'Alice', age: 30 }, { name: 'Bob', age: 25 }]
227
+
228
+ // Using custom field names (first row is data, not headers)
229
+ const data = sheet.toJson({
230
+ fields: ['name', 'age', 'city'],
231
+ });
232
+
233
+ // With TypeScript generics
234
+ interface Person {
235
+ name: string | null;
236
+ age: number | null;
237
+ }
238
+ const people = sheet.toJson<Person>();
239
+
240
+ // Starting from a specific position
241
+ const data = sheet.toJson({
242
+ startRow: 2, // Skip first 2 rows (0-based)
243
+ startCol: 1, // Start from column B
244
+ });
245
+
246
+ // Limiting the range
247
+ const data = sheet.toJson({
248
+ endRow: 10, // Stop at row 11 (0-based, inclusive)
249
+ endCol: 3, // Only read columns A-D
250
+ });
251
+
252
+ // Continue past empty rows
253
+ const data = sheet.toJson({
254
+ stopOnEmptyRow: false, // Default is true
255
+ });
256
+ ```
257
+
258
+ ### Roundtrip Example
259
+
260
+ ```typescript
261
+ // Create from objects
262
+ const originalData = [
263
+ { name: 'Alice', age: 30 },
264
+ { name: 'Bob', age: 25 },
265
+ ];
266
+
267
+ const sheet = wb.addSheetFromData({
268
+ name: 'People',
269
+ data: originalData,
270
+ });
271
+
272
+ // Read back as objects
273
+ const readData = sheet.toJson();
274
+ // readData equals originalData
275
+ ```
276
+
165
277
  ## Saving
166
278
 
167
279
  ```typescript
@@ -187,6 +299,38 @@ type CellType = 'number' | 'string' | 'boolean' | 'date' | 'error' | 'empty';
187
299
 
188
300
  // Border types
189
301
  type BorderType = 'thin' | 'medium' | 'thick' | 'double' | 'dotted' | 'dashed';
302
+
303
+ // Configuration for addSheetFromData
304
+ interface SheetFromDataConfig<T> {
305
+ name: string;
306
+ data: T[];
307
+ columns?: ColumnConfig<T>[];
308
+ headerStyle?: boolean; // Default: true
309
+ startCell?: string; // Default: 'A1'
310
+ }
311
+
312
+ interface ColumnConfig<T> {
313
+ key: keyof T;
314
+ header?: string;
315
+ style?: CellStyle;
316
+ }
317
+
318
+ // Rich cell value for formulas/styles in data
319
+ interface RichCellValue {
320
+ value?: CellValue;
321
+ formula?: string;
322
+ style?: CellStyle;
323
+ }
324
+
325
+ // Configuration for toJson
326
+ interface SheetToJsonConfig {
327
+ fields?: string[];
328
+ startRow?: number;
329
+ startCol?: number;
330
+ endRow?: number;
331
+ endCol?: number;
332
+ stopOnEmptyRow?: boolean; // Default: true
333
+ }
190
334
  ```
191
335
 
192
336
  ## Format Preservation
package/dist/index.cjs CHANGED
@@ -823,6 +823,102 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
823
823
  return this._cells;
824
824
  }
825
825
  /**
826
+ * Convert sheet data to an array of JSON objects.
827
+ *
828
+ * @param config - Configuration options
829
+ * @returns Array of objects where keys are field names and values are cell values
830
+ *
831
+ * @example
832
+ * ```typescript
833
+ * // Using first row as headers
834
+ * const data = sheet.toJson();
835
+ *
836
+ * // Using custom field names
837
+ * const data = sheet.toJson({ fields: ['name', 'age', 'city'] });
838
+ *
839
+ * // Starting from a specific row/column
840
+ * const data = sheet.toJson({ startRow: 2, startCol: 1 });
841
+ * ```
842
+ */ toJson(config = {}) {
843
+ const { fields, startRow = 0, startCol = 0, endRow, endCol, stopOnEmptyRow = true } = config;
844
+ // Get the bounds of data in the sheet
845
+ const bounds = this._getDataBounds();
846
+ if (!bounds) {
847
+ return [];
848
+ }
849
+ const effectiveEndRow = endRow ?? bounds.maxRow;
850
+ const effectiveEndCol = endCol ?? bounds.maxCol;
851
+ // Determine field names
852
+ let fieldNames;
853
+ let dataStartRow;
854
+ if (fields) {
855
+ // Use provided field names, data starts at startRow
856
+ fieldNames = fields;
857
+ dataStartRow = startRow;
858
+ } else {
859
+ // Use first row as headers
860
+ fieldNames = [];
861
+ for(let col = startCol; col <= effectiveEndCol; col++){
862
+ const cell = this._cells.get(toAddress(startRow, col));
863
+ const value = cell?.value;
864
+ fieldNames.push(value != null ? String(value) : `column${col}`);
865
+ }
866
+ dataStartRow = startRow + 1;
867
+ }
868
+ // Read data rows
869
+ const result = [];
870
+ for(let row = dataStartRow; row <= effectiveEndRow; row++){
871
+ const obj = {};
872
+ let hasData = false;
873
+ for(let colOffset = 0; colOffset < fieldNames.length; colOffset++){
874
+ const col = startCol + colOffset;
875
+ const cell = this._cells.get(toAddress(row, col));
876
+ const value = cell?.value ?? null;
877
+ if (value !== null) {
878
+ hasData = true;
879
+ }
880
+ const fieldName = fieldNames[colOffset];
881
+ if (fieldName) {
882
+ obj[fieldName] = value;
883
+ }
884
+ }
885
+ // Stop on empty row if configured
886
+ if (stopOnEmptyRow && !hasData) {
887
+ break;
888
+ }
889
+ result.push(obj);
890
+ }
891
+ return result;
892
+ }
893
+ /**
894
+ * Get the bounds of data in the sheet (min/max row and column with data)
895
+ */ _getDataBounds() {
896
+ if (this._cells.size === 0) {
897
+ return null;
898
+ }
899
+ let minRow = Infinity;
900
+ let maxRow = -Infinity;
901
+ let minCol = Infinity;
902
+ let maxCol = -Infinity;
903
+ for (const cell of this._cells.values()){
904
+ if (cell.value !== null) {
905
+ minRow = Math.min(minRow, cell.row);
906
+ maxRow = Math.max(maxRow, cell.row);
907
+ minCol = Math.min(minCol, cell.col);
908
+ maxCol = Math.max(maxCol, cell.col);
909
+ }
910
+ }
911
+ if (minRow === Infinity) {
912
+ return null;
913
+ }
914
+ return {
915
+ minRow,
916
+ maxRow,
917
+ minCol,
918
+ maxCol
919
+ };
920
+ }
921
+ /**
826
922
  * Generate XML for this worksheet
827
923
  */ toXml() {
828
924
  // Build sheetData from cells
@@ -2525,13 +2621,20 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
2525
2621
  * { key: 'age', header: 'Age (years)' },
2526
2622
  * ],
2527
2623
  * });
2624
+ *
2625
+ * // With rich cell values (value, formula, style)
2626
+ * const dataWithFormulas = [
2627
+ * { product: 'Widget', price: 10, qty: 5, total: { formula: 'B2*C2', style: { bold: true } } },
2628
+ * { product: 'Gadget', price: 20, qty: 3, total: { formula: 'B3*C3', style: { bold: true } } },
2629
+ * ];
2630
+ * const sheet3 = wb.addSheetFromData({
2631
+ * name: 'With Formulas',
2632
+ * data: dataWithFormulas,
2633
+ * });
2528
2634
  * ```
2529
2635
  */ addSheetFromData(config) {
2530
2636
  const { name, data, columns, headerStyle = true, startCell = 'A1' } = config;
2531
- if (data.length === 0) {
2532
- // Create empty sheet if no data
2533
- return this.addSheet(name);
2534
- }
2637
+ if (!data?.length) return this.addSheet(name);
2535
2638
  // Create the new sheet
2536
2639
  const sheet = this.addSheet(name);
2537
2640
  // Parse start cell
@@ -2562,17 +2665,41 @@ const builder = new fastXmlParser.XMLBuilder(builderOptions);
2562
2665
  const colConfig = columnConfigs[colIdx];
2563
2666
  const value = rowData[colConfig.key];
2564
2667
  const cell = sheet.cell(startRow + rowIdx, startCol + colIdx);
2565
- // Convert value to CellValue
2566
- cell.value = this._toCellValue(value);
2567
- // Apply column style if defined
2668
+ // Check if value is a rich cell definition
2669
+ if (this._isRichCellValue(value)) {
2670
+ const richValue = value;
2671
+ if (richValue.value !== undefined) cell.value = richValue.value;
2672
+ if (richValue.formula !== undefined) cell.formula = richValue.formula;
2673
+ if (richValue.style !== undefined) cell.style = richValue.style;
2674
+ } else {
2675
+ // Convert value to CellValue
2676
+ cell.value = this._toCellValue(value);
2677
+ }
2678
+ // Apply column style if defined (merged with cell style)
2568
2679
  if (colConfig.style) {
2569
- cell.style = colConfig.style;
2680
+ cell.style = {
2681
+ ...cell.style,
2682
+ ...colConfig.style
2683
+ };
2570
2684
  }
2571
2685
  }
2572
2686
  }
2573
2687
  return sheet;
2574
2688
  }
2575
2689
  /**
2690
+ * Check if a value is a rich cell value object with value, formula, or style fields
2691
+ */ _isRichCellValue(value) {
2692
+ if (value === null || value === undefined) {
2693
+ return false;
2694
+ }
2695
+ if (typeof value !== 'object' || value instanceof Date) {
2696
+ return false;
2697
+ }
2698
+ // Check if it has at least one of the rich cell properties
2699
+ const obj = value;
2700
+ return 'value' in obj || 'formula' in obj || 'style' in obj;
2701
+ }
2702
+ /**
2576
2703
  * Infer column configuration from the first data object
2577
2704
  */ _inferColumns(sample) {
2578
2705
  return Object.keys(sample).map((key)=>({
package/dist/index.d.cts CHANGED
@@ -156,6 +156,51 @@ interface ColumnConfig<T = Record<string, unknown>> {
156
156
  /** Cell style for data cells in this column */
157
157
  style?: CellStyle;
158
158
  }
159
+ /**
160
+ * Rich cell value with optional formula and style.
161
+ * Use this when you need to set value, formula, or style for individual cells.
162
+ */
163
+ interface RichCellValue {
164
+ /** Cell value */
165
+ value?: CellValue;
166
+ /** Formula (without leading '=') */
167
+ formula?: string;
168
+ /** Cell style */
169
+ style?: CellStyle;
170
+ }
171
+ /**
172
+ * Configuration for converting a sheet to JSON objects.
173
+ */
174
+ interface SheetToJsonConfig {
175
+ /**
176
+ * Field names to use for each column.
177
+ * If provided, the first row of data starts at row 1 (or startRow).
178
+ * If not provided, the first row is used as field names.
179
+ */
180
+ fields?: string[];
181
+ /**
182
+ * Starting row (0-based). Defaults to 0.
183
+ * If fields are not provided, this row contains the headers.
184
+ * If fields are provided, this is the first data row.
185
+ */
186
+ startRow?: number;
187
+ /**
188
+ * Starting column (0-based). Defaults to 0.
189
+ */
190
+ startCol?: number;
191
+ /**
192
+ * Ending row (0-based, inclusive). Defaults to the last row with data.
193
+ */
194
+ endRow?: number;
195
+ /**
196
+ * Ending column (0-based, inclusive). Defaults to the last column with data.
197
+ */
198
+ endCol?: number;
199
+ /**
200
+ * If true, stop reading when an empty row is encountered. Defaults to true.
201
+ */
202
+ stopOnEmptyRow?: boolean;
203
+ }
159
204
 
160
205
  /**
161
206
  * Represents a single cell in a worksheet
@@ -365,6 +410,29 @@ declare class Worksheet {
365
410
  * Get all cells in the worksheet
366
411
  */
367
412
  get cells(): Map<string, Cell>;
413
+ /**
414
+ * Convert sheet data to an array of JSON objects.
415
+ *
416
+ * @param config - Configuration options
417
+ * @returns Array of objects where keys are field names and values are cell values
418
+ *
419
+ * @example
420
+ * ```typescript
421
+ * // Using first row as headers
422
+ * const data = sheet.toJson();
423
+ *
424
+ * // Using custom field names
425
+ * const data = sheet.toJson({ fields: ['name', 'age', 'city'] });
426
+ *
427
+ * // Starting from a specific row/column
428
+ * const data = sheet.toJson({ startRow: 2, startCol: 1 });
429
+ * ```
430
+ */
431
+ toJson<T = Record<string, CellValue>>(config?: SheetToJsonConfig): T[];
432
+ /**
433
+ * Get the bounds of data in the sheet (min/max row and column with data)
434
+ */
435
+ private _getDataBounds;
368
436
  /**
369
437
  * Generate XML for this worksheet
370
438
  */
@@ -723,9 +791,23 @@ declare class Workbook {
723
791
  * { key: 'age', header: 'Age (years)' },
724
792
  * ],
725
793
  * });
794
+ *
795
+ * // With rich cell values (value, formula, style)
796
+ * const dataWithFormulas = [
797
+ * { product: 'Widget', price: 10, qty: 5, total: { formula: 'B2*C2', style: { bold: true } } },
798
+ * { product: 'Gadget', price: 20, qty: 3, total: { formula: 'B3*C3', style: { bold: true } } },
799
+ * ];
800
+ * const sheet3 = wb.addSheetFromData({
801
+ * name: 'With Formulas',
802
+ * data: dataWithFormulas,
803
+ * });
726
804
  * ```
727
805
  */
728
806
  addSheetFromData<T extends object>(config: SheetFromDataConfig<T>): Worksheet;
807
+ /**
808
+ * Check if a value is a rich cell value object with value, formula, or style fields
809
+ */
810
+ private _isRichCellValue;
729
811
  /**
730
812
  * Infer column configuration from the first data object
731
813
  */
@@ -810,5 +892,5 @@ declare const parseRange: (range: string) => RangeAddress;
810
892
  declare const toRange: (range: RangeAddress) => string;
811
893
 
812
894
  export { Cell, PivotCache, PivotTable, Range, SharedStrings, Styles, Workbook, Worksheet, parseAddress, parseRange, toAddress, toRange };
813
- export type { AggregationType, Alignment, BorderStyle, BorderType, CellAddress, CellError, CellStyle, CellType, CellValue, ColumnConfig, ErrorType, PivotFieldAxis, PivotTableConfig, PivotValueConfig, RangeAddress, SheetFromDataConfig };
895
+ export type { AggregationType, Alignment, BorderStyle, BorderType, CellAddress, CellError, CellStyle, CellType, CellValue, ColumnConfig, ErrorType, PivotFieldAxis, PivotTableConfig, PivotValueConfig, RangeAddress, RichCellValue, SheetFromDataConfig, SheetToJsonConfig };
814
896
  //# sourceMappingURL=index.d.cts.map