diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..c768bd543 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,95 @@ +# AGENTS.md + +Universal instructions for AI coding assistants (Claude Code, GitHub Copilot, Cursor, Gemini CLI, and others) working with the HyperFormula codebase. + +For Claude Code specific setup see [CLAUDE.md](CLAUDE.md). + +## What is HyperFormula + +HyperFormula is a **headless spreadsheet engine** — no UI, no DOM. It parses Excel-compatible formulas, evaluates them, and returns computed values. It runs in browser and Node.js. + +Core API surface (3 operations cover 90% of use): +```typescript +import { HyperFormula } from 'hyperformula'; + +const hf = HyperFormula.buildFromArray(data, { licenseKey: 'gpl-v3' }); +const values = hf.getSheetValues(0); // CellValue[][] +hf.setCellContents({ sheet: 0, row, col }, value); // RawCellContent +``` + +`licenseKey: 'gpl-v3'` is mandatory — without it the engine throws a license warning. + +## Framework integration patterns + +When helping users integrate HyperFormula with a framework, follow these idiomatic patterns: + +| Framework | Instance storage | Cleanup | Reactive bridge | SSR guard | +|---|---|---|---|---| +| React | `useRef` | `useEffect` return | `useState` | `dynamic(..., { ssr: false })` | +| Angular | `@Injectable` service with `BehaviorSubject` | `ngOnDestroy` (component-scoped) | `async` pipe | N/A (no default SSR) | +| Vue 3 | Class wrapper with private HF field | `onUnmounted` | `ref` | `` (Nuxt) | +| Svelte | top-level `const` in ` + + + +{#if result !== null} +

Result: {result}

+{/if} + + + + {#each data as row, r} + + {#each row as cell, c} + + {/each} + + {/each} + +
+ {#if hf.doesCellHaveFormula({ sheet: sheetId, row: r, col: c })} + {hf.getCellFormula({ sheet: sheetId, row: r, col: c })} + {:else} + {hf.getCellValue({ sheet: sheetId, row: r, col: c })} + {/if} +
+``` + +## Server-side rendering (SvelteKit) + +HyperFormula depends on browser-only APIs. In SvelteKit, initialize the engine inside `onMount` so the code never runs during SSR: + +```svelte + + + + +{#if result !== null} +

Result: {result}

+{/if} +``` + + +## Next steps + +- [Configuration options](configuration-options.md) — full list of `buildFromArray` / `buildEmpty` options +- [Basic operations](basic-operations.md) — CRUD on cells, rows, columns, sheets +- [Advanced usage](advanced-usage.md) — multi-sheet workbooks, named expressions +- [Custom functions](custom-functions.md) — register your own formulas ## Demo -Explore the full working example on [Stackblitz](https://stackblitz.com/github/handsontable/hyperformula-demos/tree/3.2.x/svelte-demo?v=${$page.buildDateURIEncoded}). +For a more advanced example, check out the Svelte demo on Stackblitz. diff --git a/docs/guide/integration-with-vue.md b/docs/guide/integration-with-vue.md index eaa104e0e..e8f12b93d 100644 --- a/docs/guide/integration-with-vue.md +++ b/docs/guide/integration-with-vue.md @@ -1,8 +1,86 @@ # Integration with Vue -Installing HyperFormula in a Vue application works the same as with vanilla JavaScript. +The HyperFormula API is identical in a Vue 3 app and in plain JavaScript. What changes is how you keep the engine out of Vue's reactivity system and how you surface its values into the template. -For more details, see the [client-side installation](client-side-installation.md) section. +Install with `npm install hyperformula`. For other options, see the [client-side installation](client-side-installation.md) section. + +## Basic usage + +The idiomatic Vue 3 way to encapsulate stateful logic is a [composable](https://vuejs.org/guide/reusability/composables.html). Create one function that owns the HyperFormula instance, exposes the derived state as refs, and cleans up on unmount. + +```typescript +// use-spreadsheet.ts +import { onBeforeUnmount, shallowRef } from 'vue'; +import { HyperFormula, type CellValue, type Sheet } from 'hyperformula'; + +export function useSpreadsheet(initialData: Sheet) { + // Plain `const` — Vue only proxies values passed to ref()/reactive(), + // so the engine never enters the reactivity system. See Troubleshooting + // for the case where you do need to hold it in reactive state. + const hf = HyperFormula.buildFromArray(initialData, { + licenseKey: 'gpl-v3', + // more configuration options go here + }); + + // shallowRef triggers re-renders on reassignment of `.value` but + // does not recursively proxy the grid — cheap for large result sets. + const values = shallowRef([]); + + function runCalculations() { + values.value = hf.getSheetValues(0); + } + + function reset() { + values.value = []; + } + + onBeforeUnmount(() => hf.destroy()); + + return { values, runCalculations, reset }; +} +``` + +Use the composable from a component with ` + + +``` + +Two things are doing the real work here: + +- `hf` is a plain local variable, so Vue never wraps it in a Proxy. Reactivity is opt-in in Vue 3 — only values passed through `ref`, `reactive`, `shallowRef`, etc. become reactive. +- `values` is a `shallowRef`, so the template re-renders whenever you reassign `values.value`, but the rows and cells themselves are not recursively converted to reactive Proxies. This is the right default for data you replace wholesale (as opposed to mutating in place). + +## Notes + +### Server-side rendering (Nuxt) + +HyperFormula depends on browser-only APIs and should not run during SSR. In Nuxt, wrap the component with [``](https://nuxt.com/docs/api/components/client-only) so its `setup` executes on the client only: + +```vue + +``` ## Troubleshooting @@ -14,24 +92,35 @@ If you encounter an error like Uncaught TypeError: Cannot read properties of undefined (reading 'licenseKeyValidityState') ``` -it means that Vue's reactivity system tries to deeply observe the HyperFormula instance. To fix this, wrap your HyperFormula instance in Vue's [`markRaw`](https://vuejs.org/api/reactivity-advanced.html#markraw) function: +it means Vue's reactivity system wrapped the HyperFormula instance in a `Proxy`. That proxy intercepts every property access; when it reaches a non-trivial instance with its own internal state, identity checks and lazy-initialized maps break. + +This only happens when you place the instance inside reactive state — for example, in a `reactive({...})` object, a Pinia store, or a `ref()`. The fix is to mark it raw with [`markRaw`](https://vuejs.org/api/reactivity-advanced.html#markraw) before it gets there: -```javascript +```typescript import { markRaw } from 'vue'; import { HyperFormula } from 'hyperformula'; -const hfInstance = markRaw( +const hf = markRaw( HyperFormula.buildEmpty({ - licenseKey: 'internal-use-in-handsontable', + licenseKey: 'gpl-v3', }) ); ``` -This function prevents Vue from converting the HyperFormula instance into a reactive proxy, which can cause errors and performance issues. +`markRaw` flags the object so Vue skips it on every subsequent call to `reactive`, `ref`, or similar. It must be applied to the instance itself — `shallowRef` only skips proxying at the top level, so writing the raw instance into a nested reactive structure (e.g. a Pinia store) would still wrap it. + +If you keep the instance in a plain `const` (or inside a composable, as shown [above](#basic-usage)), you do not need `markRaw` — the instance never enters the reactivity system in the first place. + +## Next steps + +- [Configuration options](configuration-options.md) — full list of `buildFromArray` / `buildEmpty` options +- [Basic operations](basic-operations.md) — CRUD on cells, rows, columns, sheets +- [Advanced usage](advanced-usage.md) — multi-sheet workbooks, named expressions +- [Custom functions](custom-functions.md) — register your own formulas ## Demo -Explore the full working example on [Stackblitz](https://stackblitz.com/github/handsontable/hyperformula-demos/tree/3.2.x/vue-3-demo?v=${$page.buildDateURIEncoded}). +For a more advanced example, check out the Vue 3 demo on Stackblitz. ::: tip This demo uses the [Vue 3](https://v3.vuejs.org/) framework. If you are looking for an example using Vue 2, check out the [code on GitHub](https://github.com/handsontable/hyperformula-demos/tree/2.5.x/vue-demo).