-
Notifications
You must be signed in to change notification settings - Fork 156
HF-122: Framework integration guides for React, Angular, Vue, Svelte #1653
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
marcin-kordas-hoc
wants to merge
16
commits into
develop
Choose a base branch
from
feature/hf-122-framework-integration-guides
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+526
−17
Open
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
27b9582
Docs: add basic usage snippets to framework integration guides
marcin-kordas-hoc e920dcc
Docs: simplify snippets and clarify Demo link wording
marcin-kordas-hoc 88056b7
Docs: modernize framework integration guides with idiomatic patterns
marcin-kordas-hoc 9571737
Docs: add Next steps cross-links to framework integration guides
marcin-kordas-hoc bd84ce9
Docs: fix $page.buildDateURIEncoded template syntax in framework guides
marcin-kordas-hoc 87c2f46
Docs: fix setCellContents type in guide snippets (unknown → RawCellCo…
marcin-kordas-hoc 2f8b2be
Docs: add AGENTS.md with universal AI assistant instructions
marcin-kordas-hoc 2a07790
Merge branch 'develop' into feature/hf-122-framework-integration-guides
sequba 683bc08
Docs: align Angular and Svelte guide snippets with Stackblitz demos
marcin-kordas-hoc 655274a
Docs: fix P0/P1 issues found in framework guide expert review
marcin-kordas-hoc b2cb63d
Docs: align guides with Stackblitz demos and fix review findings
marcin-kordas-hoc 76bcfdd
Docs: fix Stackblitz links using Vue v-bind instead of template inter…
marcin-kordas-hoc f8dfa07
Docs: remove untested framework patterns not present in demos
marcin-kordas-hoc 545ea1e
Docs: align all guide snippets with demo interaction patterns
marcin-kordas-hoc af96abc
Docs: fix AGENTS.md Vue SSR guard and align Svelte button labels
marcin-kordas-hoc d6a316c
Merge branch 'develop' into feature/hf-122-framework-integration-guides
sequba File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<HyperFormula>` | `useEffect` return | `useState<CellValue[][]>` | `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<CellValue[][]>` | `<ClientOnly>` (Nuxt) | | ||
| | Svelte | top-level `const` in `<script>` | `onDestroy` | plain `let` (Svelte 4) | `onMount` + dynamic import | | ||
|
|
||
| Critical rules: | ||
| - **Vue:** keep the HF instance inside a wrapper class — Vue's Proxy breaks HF internal state (see Troubleshooting in guide) | ||
| - **Svelte:** always `onDestroy(() => hf.destroy())` — omitting it leaks the engine | ||
| - **React:** pattern survives `StrictMode` double-invocation (mount→unmount→mount) | ||
| - **Angular:** `providedIn: 'root'` services live for the app lifetime — scope to component for per-feature cleanup | ||
| - **SSR:** HF depends on browser-only APIs — guard with framework's client-only mechanism | ||
|
|
||
| Full guide with TypeScript snippets: `docs/guide/integration-with-{react,angular,vue,svelte}.md` | ||
|
|
||
| ## Project structure | ||
|
|
||
| | Path | Description | | ||
| |---|---| | ||
| | `src/HyperFormula.ts` | Main engine class, public API | | ||
| | `src/interpreter/plugin/` | All function implementations (extend `FunctionPlugin`) | | ||
| | `src/parser/` | Chevrotain-based formula parser | | ||
| | `src/DependencyGraph/` | Cell dependency tracking | | ||
| | `src/i18n/languages/` | Function name translations (17 languages) | | ||
| | `test/unit/` | Jest tests (`*.spec.ts`) | | ||
| | `docs/guide/` | User-facing documentation (VuePress) | | ||
| | `docs/examples/` | 49 inline code examples rendered in docs | | ||
| | `typings/` | Generated `.d.ts` type declarations | | ||
|
|
||
| ## Adding a new function | ||
|
|
||
| 1. Create or modify plugin in `src/interpreter/plugin/` | ||
| 2. Add metadata to `implementedFunctions` static property | ||
| 3. Add translations to all 17 files in `src/i18n/languages/` | ||
| 4. Add tests in `test/unit/interpreter/` | ||
| 5. Use `runFunction()` helper for argument validation and coercion | ||
|
|
||
| ## Key types | ||
|
|
||
| ```typescript | ||
| CellValue // number | string | boolean | DetailedCellError | null (output from getSheetValues) | ||
| RawCellContent // string | number | boolean | Date | null | undefined | RawCellContent[][] (input to setCellContents) | ||
| SimpleCellAddress // { sheet: number, row: number, col: number } | ||
| ``` | ||
|
|
||
| ## Common mistakes to prevent | ||
|
|
||
| - Passing HF instance into Vue `reactive()` / `ref()` without `markRaw` → cryptic TypeError | ||
| - Forgetting `licenseKey` in config → silent warning, no crash, confusing for users | ||
| - Using `unknown` type for `setCellContents` value arg → use `RawCellContent` | ||
| - SSR: importing `hyperformula` at module scope in Next.js/Nuxt/SvelteKit → server crash | ||
| - Array functions: HF uses **parse-time array sizing** — output dimensions determined before evaluation | ||
|
|
||
| ## Build and test | ||
|
|
||
| ```bash | ||
| npm install # Install dependencies | ||
| npm run test:unit # Jest unit tests (fast) | ||
| npm run lint # ESLint | ||
| npm run bundle-all # Full build (compile + bundle) | ||
| npm run docs:dev # Local docs preview (VuePress, port 8080) | ||
| ``` | ||
|
|
||
| ## Contributing | ||
|
|
||
| - Branch from `develop`, never commit directly to master | ||
| - Tests required for all changes in `test/` folder | ||
| - Run linter before submitting | ||
| - Maintain compatibility with Excel and Google Sheets behavior |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,9 +1,122 @@ | ||||||
| # Integration with Angular | ||||||
|
|
||||||
| Installing HyperFormula in an Angular application works the same as with vanilla JavaScript. | ||||||
| The HyperFormula API is identical in an Angular app and in plain JavaScript. What changes is where the engine lives (typically an injectable service), how it is cleaned up, and how you bridge its values into the change-detection cycle. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I don't like the phrase "What changes is..." |
||||||
|
|
||||||
| 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 engine in an `@Injectable` service backed by a `BehaviorSubject`. Components subscribe to the observable with the `async` pipe, which handles subscription cleanup automatically. | ||||||
|
|
||||||
| ```typescript | ||||||
| // spreadsheet.service.ts | ||||||
| import { Injectable } from '@angular/core'; | ||||||
| import { BehaviorSubject } from 'rxjs'; | ||||||
| import { HyperFormula, type CellValue } from 'hyperformula'; | ||||||
|
|
||||||
| @Injectable({ providedIn: 'root' }) | ||||||
| export class SpreadsheetService { | ||||||
| private readonly hf: HyperFormula; | ||||||
|
|
||||||
| private readonly _values = new BehaviorSubject<CellValue[][]>([]); | ||||||
| readonly values$ = this._values.asObservable(); | ||||||
|
|
||||||
| constructor() { | ||||||
| this.hf = HyperFormula.buildFromArray( | ||||||
| [ | ||||||
| [1, 2, '=A1+B1'], | ||||||
| // your data rows go here | ||||||
| ], | ||||||
| { | ||||||
| licenseKey: 'gpl-v3', | ||||||
| // more configuration options go here | ||||||
| } | ||||||
| ); | ||||||
| this._values.next(this.hf.getSheetValues(0)); | ||||||
| } | ||||||
|
|
||||||
| calculate() { | ||||||
| this._values.next(this.hf.getSheetValues(0)); | ||||||
| } | ||||||
|
|
||||||
| reset() { | ||||||
| this._values.next([]); | ||||||
| } | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| Consume the service from a component and bind `values$ | async` in the template. Declare the component in your `AppModule` alongside `CommonModule`: | ||||||
|
|
||||||
| ```typescript | ||||||
| // spreadsheet.component.ts | ||||||
| import { Component } from '@angular/core'; | ||||||
| import { Observable } from 'rxjs'; | ||||||
| import { SpreadsheetService } from './spreadsheet.service'; | ||||||
| import { type CellValue } from 'hyperformula'; | ||||||
|
|
||||||
| @Component({ | ||||||
| selector: 'app-spreadsheet', | ||||||
| templateUrl: './spreadsheet.component.html', | ||||||
| }) | ||||||
| export class SpreadsheetComponent { | ||||||
| values$: Observable<CellValue[][]>; | ||||||
|
|
||||||
| constructor(private spreadsheetService: SpreadsheetService) { | ||||||
| this.values$ = this.spreadsheetService.values$; | ||||||
| } | ||||||
|
|
||||||
| runCalculations() { | ||||||
| this.spreadsheetService.calculate(); | ||||||
| } | ||||||
|
|
||||||
| reset() { | ||||||
| this.spreadsheetService.reset(); | ||||||
| } | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| ```html | ||||||
| <!-- spreadsheet.component.html --> | ||||||
| <button (click)="runCalculations()">Run calculations</button> | ||||||
| <button (click)="reset()">Reset</button> | ||||||
| <table *ngIf="(values$ | async) as values"> | ||||||
| <tr *ngFor="let row of values"> | ||||||
| <td *ngFor="let cell of row">{{ cell }}</td> | ||||||
| </tr> | ||||||
| </table> | ||||||
| ``` | ||||||
|
|
||||||
| ## Notes | ||||||
|
|
||||||
| ### Provider scope | ||||||
|
|
||||||
| `providedIn: 'root'` makes the service an application-wide singleton — suitable when a single HyperFormula instance is shared across the app. For per-feature or per-component instances (for example, several independent reports on one screen), provide the service at the component level via `providers: [SpreadsheetService]`; the service is then created and destroyed alongside the component. | ||||||
|
|
||||||
| ### Cleanup | ||||||
|
|
||||||
| Root-scoped services live for the application's full lifetime — `ngOnDestroy` fires only at app shutdown. If you scope the service to a component (`providers: [SpreadsheetService]`), implement `OnDestroy` to release the engine: | ||||||
|
|
||||||
| ```typescript | ||||||
| import { Injectable, OnDestroy } from '@angular/core'; | ||||||
|
|
||||||
| @Injectable() | ||||||
| export class SpreadsheetService implements OnDestroy { | ||||||
| // ... | ||||||
|
|
||||||
| ngOnDestroy() { | ||||||
| this.hf.destroy(); | ||||||
| } | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
|
|
||||||
| ## 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/angular-demo?v=${$page.buildDateURIEncoded}). | ||||||
| For a more advanced example, check out the <a :href="'https://stackblitz.com/github/handsontable/hyperformula-demos/tree/3.2.x/angular-demo?v=' + $page.buildDateURIEncoded">Angular demo on Stackblitz</a>. | ||||||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,9 +1,112 @@ | ||||||
| # Integration with React | ||||||
|
|
||||||
| Installing HyperFormula in a React application works the same as with vanilla JavaScript. | ||||||
| The HyperFormula API is identical in a React app and in plain JavaScript. What changes is where the engine lives in a component tree and how its lifecycle maps to React hooks. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
|
||||||
| 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 | ||||||
|
|
||||||
| Hold the HyperFormula instance in a `useRef` so it survives re-renders. Initialize it inside `useEffect` and release it in the cleanup function. Use `useState` to toggle between raw formulas and computed values. | ||||||
|
|
||||||
| ```tsx | ||||||
| import { useEffect, useRef, useState } from 'react'; | ||||||
| import { HyperFormula, CellValue } from 'hyperformula'; | ||||||
|
|
||||||
| export function SpreadsheetComponent() { | ||||||
| const hfRef = useRef<HyperFormula | null>(null); | ||||||
| const [values, setValues] = useState<CellValue[][]>([]); | ||||||
|
|
||||||
| useEffect(() => { | ||||||
| const hf = HyperFormula.buildFromArray( | ||||||
| [ | ||||||
| [1, 2, '=A1+B1'], | ||||||
| // your data rows go here | ||||||
| ], | ||||||
| { | ||||||
| licenseKey: 'gpl-v3', | ||||||
| // more configuration options go here | ||||||
| } | ||||||
| ); | ||||||
| hfRef.current = hf; | ||||||
|
|
||||||
| return () => { | ||||||
| hf.destroy(); | ||||||
| hfRef.current = null; | ||||||
| }; | ||||||
| }, []); | ||||||
|
|
||||||
| function runCalculations() { | ||||||
| if (!hfRef.current) return; | ||||||
| setValues(hfRef.current.getSheetValues(0)); | ||||||
| } | ||||||
|
|
||||||
| function reset() { | ||||||
| setValues([]); | ||||||
| } | ||||||
|
|
||||||
| return ( | ||||||
| <> | ||||||
| <button onClick={runCalculations}>Run calculations</button> | ||||||
| <button onClick={reset}>Reset</button> | ||||||
| {values.length > 0 && ( | ||||||
| <table> | ||||||
| <tbody> | ||||||
| {values.map((row, r) => ( | ||||||
| <tr key={r}> | ||||||
| {row.map((cell, c) => ( | ||||||
| <td key={c}>{String(cell ?? '')}</td> | ||||||
| ))} | ||||||
| </tr> | ||||||
| ))} | ||||||
| </tbody> | ||||||
| </table> | ||||||
| )} | ||||||
| </> | ||||||
| ); | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| If you use JavaScript instead of TypeScript, drop the type annotations — the rest of the pattern is unchanged. | ||||||
|
|
||||||
| ## Notes | ||||||
|
|
||||||
| ### `React.StrictMode` double invocation | ||||||
|
|
||||||
| In development, React 18 runs effects twice (mount → unmount → mount) to surface cleanup bugs. The pattern above is correct for StrictMode because `destroy()` runs before the re-mount creates a new instance, so no work leaks between the two lifecycles. Do not switch to a module-scoped singleton as a workaround — it will break StrictMode semantics. | ||||||
|
|
||||||
| ### Server-side rendering (Next.js) | ||||||
|
|
||||||
| HyperFormula depends on browser-only APIs. Mark `SpreadsheetComponent.tsx` with `'use client'`, then load it lazily from a **server** page component using `dynamic(..., { ssr: false })`: | ||||||
|
|
||||||
| ```tsx | ||||||
| // app/spreadsheet/SpreadsheetComponent.tsx | ||||||
| 'use client'; | ||||||
| // ... component definition as above | ||||||
| ``` | ||||||
|
|
||||||
| ```tsx | ||||||
| // app/spreadsheet/page.tsx ← server component, no 'use client' | ||||||
| import dynamic from 'next/dynamic'; | ||||||
|
|
||||||
| const SpreadsheetComponent = dynamic( | ||||||
| () => import('./SpreadsheetComponent').then((m) => m.SpreadsheetComponent), | ||||||
| { ssr: false } | ||||||
| ); | ||||||
|
|
||||||
| export default function Page() { | ||||||
| return <SpreadsheetComponent />; | ||||||
| } | ||||||
| ``` | ||||||
|
|
||||||
| Putting `'use client'` on `page.tsx` itself would make the entire page a client component — the point is to keep the page server-rendered and only hydrate the spreadsheet widget on the client. In the Pages Router, the same `dynamic(..., { ssr: false })` pattern works without `'use client'`. | ||||||
|
|
||||||
| ## 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/react-demo?v=${$page.buildDateURIEncoded}). | ||||||
| For a more advanced example, check out the <a :href="'https://stackblitz.com/github/handsontable/hyperformula-demos/tree/3.2.x/react-demo?v=' + $page.buildDateURIEncoded">React demo on Stackblitz</a>. | ||||||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why was it changed?