| 
									
										
										
										
											2024-07-16 01:40:51 +00:00
										 |  |  | --- | 
					
						
							|  |  |  | title: Merged Cells | 
					
						
							|  |  |  | sidebar_position: 11 | 
					
						
							|  |  |  | --- | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | <details> | 
					
						
							|  |  |  |   <summary><b>File Format Support</b> (click to show)</summary> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | By default, no cells are merged. Merge metadata is ignored when exporting to a | 
					
						
							|  |  |  | file format that does not support merge cells. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Formats store the actual contents of a merged cell in the "top-left" corner | 
					
						
							|  |  |  | (first row and first column of the merge range). Some formats can hold data for | 
					
						
							|  |  |  | cells that are covered by the merge range. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | | Formats   | Merge | Covered | | 
					
						
							|  |  |  | |:----------|:-----:|:-------:| | 
					
						
							|  |  |  | | XLSX/XLSM |   ✔   |    ✔    | | 
					
						
							|  |  |  | | XLSB      |   ✔   |    ✔    | | 
					
						
							|  |  |  | | XLML      |   ✔   |    ✔    | | 
					
						
							|  |  |  | | BIFF8 XLS |   ✔   |    ✔    | | 
					
						
							|  |  |  | | ODS/FODS  |   ✔   |    ✔    | | 
					
						
							|  |  |  | | NUMBERS   |   ✔   |    ✔    | | 
					
						
							|  |  |  | | HTML      |   ✔   |         | | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | There are multiple representations of merge cells in the NUMBERS file format. | 
					
						
							|  |  |  | Writers use the simplified `.TST.MergeRegionMapArchive` representation. Parsers | 
					
						
							|  |  |  | understand the classic form and the modern `.TST.MergeOwnerArchive` form. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | </details> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Modern spreadsheet software typically allow users to combine blocks of cells | 
					
						
							|  |  |  | into a single unit. This unit can span multiple columns and rows. As shown in | 
					
						
							|  |  |  | the following table, HTML TH and TD elements use `colspan` and `rowspan` | 
					
						
							|  |  |  | attributes to effectuate merging: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | <table><tbody> | 
					
						
							|  |  |  |   <tr><td colSpan="4"><center>This title spans four columns</center></td></tr> | 
					
						
							|  |  |  |   <tr><td>SheetJS</td><td>supports</td><td>merge</td><td>cells</td></tr> | 
					
						
							|  |  |  | </tbody></table> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | :::tip pass | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | This feature was expanded in version `0.20.3`. It is strongly recommended to | 
					
						
							|  |  |  | [upgrade to the latest version](/docs/getting-started/installation/). | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ::: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ## Storage
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The `!merges` property of the worksheet object is expected to be an array of | 
					
						
							|  |  |  | [SheetJS range objects](/docs/csf/general#sheetjs-range). Each range object | 
					
						
							|  |  |  | corresponds to a merged range in the worksheet. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The following snippet creates a merge range spanning `A1:B2` : | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```js title="Merge the range A1:B2 in a worksheet" | 
					
						
							|  |  |  | ws["!merges"] = [ | 
					
						
							|  |  |  |   { s: { c: 0, r: 0 }, e: { c: 1, r: 1 } }  // A1:B2 | 
					
						
							|  |  |  | ]; | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | :::caution pass | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | **Overlapping merges are not automatically detected!** | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ::: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ### Range
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The [`decode_range`](/docs/csf/general#cell-ranges-1) method creates range | 
					
						
							|  |  |  | objects from A1-style range strings. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The following snippet creates a merge range spanning `A1:B2` : | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```js title="Merge the range A1:B2 in a worksheet" | 
					
						
							|  |  |  | ws["!merges"] = [ | 
					
						
							|  |  |  |   XLSX.utils.decode_range("A1:B2") | 
					
						
							|  |  |  | ]; | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ### Overlap
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | When adding merges to an existing workbook, it is strongly recommended to scan | 
					
						
							|  |  |  | the merges array and test for collisions: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```js title="Add a merged range to a worksheet" | 
					
						
							|  |  |  | function sheet_add_merge(ws, range) { | 
					
						
							|  |  |  |   /* if `range` is a string, parse into a range object */ | 
					
						
							|  |  |  |   var merge = typeof range == "string" ? XLSX.utils.decode_range(range) : range; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /* create array merge if it does not exist */ | 
					
						
							|  |  |  |   if(!ws["!merges"]) ws["!merges"] = []; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /* check if the new merge collides with any existing merge */ | 
					
						
							|  |  |  |   ws["!merges"].forEach(function(range) { | 
					
						
							|  |  |  |     if(merge.e.r < range.s.r) return; | 
					
						
							|  |  |  |     if(range.e.r < merge.s.r) return; | 
					
						
							|  |  |  |     if(merge.e.c < range.s.c) return; | 
					
						
							|  |  |  |     if(range.e.c < merge.s.c) return; | 
					
						
							|  |  |  |     throw new Error(XLSX.utils.encode_range(merge)+" overlaps "+XLSX.utils.encode_range(range)); | 
					
						
							|  |  |  |   }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /* add merge */ | 
					
						
							|  |  |  |   ws["!merges"].push(merge); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ### Cells
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Spreadsheet tools will store and use the top-left cell of a merge range. For | 
					
						
							|  |  |  | example, if the range `B2:C5` is merged, the cell corresponding to the range | 
					
						
							|  |  |  | will be stored in the worksheet in cell `B2`. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #### Covered Cells
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | Spreadsheet tools can store cells that are covered by a merged cell. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The [SheetJS worksheet object](/docs/csf/sheet) can store covered cells. | 
					
						
							|  |  |  | [API Functions](#functions) may omit or include covered cells. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ## Live Demo
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | This example generates a worksheet that matches the following screenshot: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |  | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | The merge ranges are `A1:B2`, `C1:C2`, `A3:B3`, `D1:D2`, and `A4:B4`. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```jsx live | 
					
						
							|  |  |  | function SheetJSMergeCellsExport() { return (<button onClick={() => { | 
					
						
							|  |  |  |   /* write data to the top-left corner of each range */ | 
					
						
							|  |  |  |   var ws = XLSX.utils.aoa_to_sheet([ | 
					
						
							|  |  |  |     ["A1:B2",  /* B1 */, "C1:C2",  "Separate blocks"], // row 1 | 
					
						
							|  |  |  |     [],                                                // row 2 | 
					
						
							|  |  |  |     ["A3:B3",  /* B3 */, "C3"],                        // row 3 | 
					
						
							|  |  |  |     ["... are merged separately"],                     // row 4 | 
					
						
							|  |  |  |   ]); | 
					
						
							|  |  |  |   /* add merges */ | 
					
						
							|  |  |  |   ws["!merges"] = [ | 
					
						
							|  |  |  |     { s: { c: 0, r: 0 }, e: { c: 1, r: 1 } },  // A1:B2 | 
					
						
							|  |  |  |     { s: { c: 2, r: 0 }, e: { c: 2, r: 1 } },  // C1:C2 | 
					
						
							|  |  |  |     { s: { c: 0, r: 2 }, e: { c: 1, r: 2 } },  // A3:B3 | 
					
						
							|  |  |  |     { s: { c: 3, r: 0 }, e: { c: 3, r: 1 } },  // D1:D2 | 
					
						
							|  |  |  |     { s: { c: 0, r: 3 }, e: { c: 1, r: 3 } }   // A4:B4 | 
					
						
							|  |  |  |   ]; | 
					
						
							|  |  |  |   /* export to XLSX */ | 
					
						
							|  |  |  |   var wb = XLSX.utils.book_new(ws, "Merges"); | 
					
						
							|  |  |  |   XLSX.writeFile(wb, "SheetJSMergeCells.xlsx"); | 
					
						
							|  |  |  | }}><b>Click here to Export</b></button>); } | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ## Functions
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #### HTML
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | [`table_to_sheet` and `table_to_book`](/docs/api/utilities/html#html-table-input) | 
					
						
							|  |  |  | will generate worksheets that include merged ranges: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | <details> | 
					
						
							|  |  |  |   <summary><b>Live Demo</b> (click to show)</summary> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```jsx live | 
					
						
							|  |  |  | function SheetJSDOMMergedCells() { | 
					
						
							|  |  |  |   const ref = React.useRef(null); | 
					
						
							|  |  |  |   const [ merges, setMerges ] = React.useState([]); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   React.useEffect(() => { | 
					
						
							|  |  |  |     if(ref.current) { | 
					
						
							|  |  |  |       const tbl = ref.current.getElementsByTagName("TABLE"); | 
					
						
							|  |  |  |       if(!tbl || !tbl[0]) return; | 
					
						
							|  |  |  |       const ws = XLSX.utils.table_to_sheet(tbl[0]); | 
					
						
							|  |  |  |       console.log(ws["!merges"]) | 
					
						
							|  |  |  |       setMerges(ws["!merges"] || []); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   }, [ref]) | 
					
						
							|  |  |  |   const ws = XLSX.utils.aoa_to_sheet([ | 
					
						
							|  |  |  |     ["A1:B1 is merged", "This cell is covered" ], | 
					
						
							|  |  |  |     ["A2 is not merged", "B2 is not merged"] | 
					
						
							|  |  |  |   ]); | 
					
						
							|  |  |  |   ws["!merges"] = [XLSX.utils.decode_range("A1:B1")]; | 
					
						
							|  |  |  |   const __html = XLSX.utils.sheet_to_html(ws); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return ( <> | 
					
						
							|  |  |  |     <b>Table:</b> | 
					
						
							|  |  |  |     <div ref={ref} dangerouslySetInnerHTML={{__html}}/> | 
					
						
							|  |  |  |     <b>Merges:</b> | 
					
						
							|  |  |  |     <pre>{merges ? merges.map(m =>  XLSX.utils.encode_range(m)).join("\n") : ""}</pre> | 
					
						
							| 
									
										
										
										
											2024-09-22 07:31:02 +00:00
										 |  |  |   </> ); | 
					
						
							| 
									
										
										
										
											2024-07-16 01:40:51 +00:00
										 |  |  | } | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | </details> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | [`sheet_to_html`](/docs/api/utilities/html#html-table-output) will generate HTML | 
					
						
							|  |  |  | strings that use `colspan` and `rowspan` for merged ranges: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | <details> | 
					
						
							|  |  |  |   <summary><b>Live Demo</b> (click to show)</summary> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```jsx live | 
					
						
							|  |  |  | function SheetJSHTMLMergedCells() { | 
					
						
							|  |  |  |   const ws = XLSX.utils.aoa_to_sheet([ | 
					
						
							|  |  |  |     ["A1:B1 is merged", "This cell is covered" ], | 
					
						
							|  |  |  |     ["A2 is not merged", "B2 is not merged"] | 
					
						
							|  |  |  |   ]); | 
					
						
							|  |  |  |   ws["!merges"] = [XLSX.utils.decode_range("A1:B1")]; | 
					
						
							|  |  |  |   const __html = XLSX.utils.sheet_to_html(ws); | 
					
						
							|  |  |  |   return ( <div dangerouslySetInnerHTML={{__html}}/> ); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | </details> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #### Reading Files
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | [`read` and `readFile`](/docs/api/parse-options) will extract merge metadata | 
					
						
							|  |  |  | from supported files. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #### Writing Files
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | [`write` and `writeFile`](/docs/api/write-options) will attempt to write merge | 
					
						
							|  |  |  | metadata when exporting to file formats that support merged ranges. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | When writing to CSV and other formats that do not support merged ranges, every | 
					
						
							|  |  |  | cell in the range will be exported. This includes covered cells! | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | <details> | 
					
						
							|  |  |  |   <summary><b>Live Demo</b> (click to show)</summary> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```jsx live | 
					
						
							|  |  |  | function SheetJSCSVMergedCells() { | 
					
						
							|  |  |  |   const ws = XLSX.utils.aoa_to_sheet([ | 
					
						
							|  |  |  |     ["A1:B1 is merged", "This cell is covered" ], | 
					
						
							|  |  |  |     ["A2 is not merged", "B2 is not merged"] | 
					
						
							|  |  |  |   ]); | 
					
						
							|  |  |  |   ws["!merges"] = [XLSX.utils.decode_range("A1:B1")]; | 
					
						
							|  |  |  |   const wb = XLSX.utils.book_new(ws, "Sheet1"); | 
					
						
							|  |  |  |   const csv = XLSX.write(wb, { type: "string", bookType: "csv"}); | 
					
						
							|  |  |  |   return ( <pre>{csv}</pre> ); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | </details> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | #### Exporting Data
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | [`sheet_to_csv`](/docs/api/utilities/csv#delimiter-separated-output) and | 
					
						
							|  |  |  | [`sheet_to_json`](/docs/api/utilities/array#array-output) do not support merged | 
					
						
							|  |  |  | ranges. The exports will include covered cells: | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | <details> | 
					
						
							|  |  |  |   <summary><b>Live Demo</b> (click to show)</summary> | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ```jsx live | 
					
						
							|  |  |  | function SheetJSAOAMergedCells() { | 
					
						
							|  |  |  |   const ws = XLSX.utils.aoa_to_sheet([ | 
					
						
							|  |  |  |     ["A1:B1 is merged", "This cell is covered" ], | 
					
						
							|  |  |  |     ["A2 is not merged", "B2 is not merged"] | 
					
						
							|  |  |  |   ]); | 
					
						
							|  |  |  |   ws["!merges"] = [XLSX.utils.decode_range("A1:B1")]; | 
					
						
							|  |  |  |   const aoa = XLSX.utils.sheet_to_json(ws, {header:1}); | 
					
						
							|  |  |  |   return ( <pre>{JSON.stringify(aoa,2,2)}</pre> ); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | ``` | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | </details> |