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} +
+``` + +A few things worth calling out: + +- `hf` stays a plain `const` — it's a reference that never changes, so it doesn't need `$state`. Svelte 5 doesn't deeply proxy class instances passed to `$state`, so storing an engine there is safe if you do need re-assignability, but don't reach for it without a reason. +- The table cell expressions read no reactive state, so they run once at render time. If you later mutate the sheet via `hf.setCellContents(...)` and want the table to refresh, copy the rows you need into a `$state` (or a `$derived`) and update that value after each mutation. + +If you prefer plain JavaScript, drop `lang="ts"` and the type annotations — the runtime behavior is unchanged. + +## Server-side rendering (SvelteKit) + +HyperFormula is a heavyweight engine that has no role in server-rendered markup. Build it inside `onMount`, which only runs in the browser, and release it in `onDestroy`. + +`onMount` is allowed to be `async`, but its cleanup function must be returned **synchronously** — an async callback always returns a Promise, so any cleanup you `return` from it is silently ignored. Put the teardown in a separate `onDestroy` instead. + +```svelte + + + + +{#if result !== null} +

Result: {result}

+{/if} +``` + +Two details that matter for correctness: + +- **`$state` around `hf`** — the engine is assigned asynchronously inside `onMount`, so the variable needs to be reactive for `disabled={!hf}` to flip once the engine is ready. Svelte 5 does not deep-proxy class instances stored in `$state`, so the HyperFormula instance remains untouched; only the reassignment is tracked. +- **Dynamic `await import('hyperformula')` + type-only `import type`** — the runtime import keeps the module out of the server bundle, while `import type` is erased at compile time, so it adds nothing to the server payload while still giving you full type checking. + +## 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..92286531a 100644 --- a/docs/guide/integration-with-vue.md +++ b/docs/guide/integration-with-vue.md @@ -1,8 +1,85 @@ # 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 + +Wrap the HyperFormula instance inside a plain class so it stays outside Vue's reactivity system (see [Troubleshooting](#vue-reactivity-issues) below for why this matters). Hold derived data in `ref` so the template updates when you reassign the ref's `.value`. + +```typescript +// spreadsheet-provider.ts +import { HyperFormula, type CellValue } from 'hyperformula'; + +export class SpreadsheetProvider { + private hf: HyperFormula; + + constructor(data: (string | number | null)[][]) { + this.hf = HyperFormula.buildFromArray(data, { + licenseKey: 'gpl-v3', + // more configuration options go here + }); + } + + getCalculatedValues(): CellValue[][] { + return this.hf.getSheetValues(0); + } + + getRawFormulas(): (string | number | null)[][] { + return this.hf.getSheetSerialized(0) as (string | number | null)[][]; + } + + destroy() { + this.hf.destroy(); + } +} +``` + +Use the class from a component with ` + + +``` + +The class keeps the HyperFormula instance as a private field, so Vue's reactivity Proxy never reaches it. This is the same pattern used in the [Vue 3 demo](#demo). + +## Notes + +### Server-side rendering (Nuxt) + +HyperFormula depends on browser-only APIs. In Nuxt, render the spreadsheet on the client only by wrapping the component with ``. ## Troubleshooting @@ -14,24 +91,31 @@ 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 that Vue's reactivity system tried to deeply observe the HyperFormula instance. Vue wraps reactive objects in a `Proxy` that intercepts every property access; when that proxy reaches a non-trivial instance with its own internal state, identity checks and lazy-initialized maps break. The fix is to opt the instance out of reactivity with Vue's [`markRaw`](https://vuejs.org/api/reactivity-advanced.html#markraw): -```javascript +```typescript import { markRaw } from 'vue'; import { HyperFormula } from 'hyperformula'; const hfInstance = 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. +`shallowRef` is not a substitute: it skips proxying only at the top level, so writing the instance into a nested reactive structure (Pinia state, `reactive({...})`) will still wrap it. Always pass the instance itself through `markRaw` before putting it anywhere Vue can reach. + +## 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).