Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
27b9582
Docs: add basic usage snippets to framework integration guides
marcin-kordas-hoc Apr 9, 2026
e920dcc
Docs: simplify snippets and clarify Demo link wording
marcin-kordas-hoc Apr 13, 2026
88056b7
Docs: modernize framework integration guides with idiomatic patterns
marcin-kordas-hoc Apr 13, 2026
9571737
Docs: add Next steps cross-links to framework integration guides
marcin-kordas-hoc Apr 13, 2026
bd84ce9
Docs: fix $page.buildDateURIEncoded template syntax in framework guides
marcin-kordas-hoc Apr 13, 2026
87c2f46
Docs: fix setCellContents type in guide snippets (unknown → RawCellCo…
marcin-kordas-hoc Apr 14, 2026
2f8b2be
Docs: add AGENTS.md with universal AI assistant instructions
marcin-kordas-hoc Apr 14, 2026
2a07790
Merge branch 'develop' into feature/hf-122-framework-integration-guides
sequba Apr 14, 2026
683bc08
Docs: align Angular and Svelte guide snippets with Stackblitz demos
marcin-kordas-hoc Apr 15, 2026
655274a
Docs: fix P0/P1 issues found in framework guide expert review
marcin-kordas-hoc Apr 15, 2026
b2cb63d
Docs: align guides with Stackblitz demos and fix review findings
marcin-kordas-hoc Apr 16, 2026
76bcfdd
Docs: fix Stackblitz links using Vue v-bind instead of template inter…
marcin-kordas-hoc Apr 16, 2026
f8dfa07
Docs: remove untested framework patterns not present in demos
marcin-kordas-hoc Apr 16, 2026
545ea1e
Docs: align all guide snippets with demo interaction patterns
marcin-kordas-hoc Apr 16, 2026
af96abc
Docs: fix AGENTS.md Vue SSR guard and align Svelte button labels
marcin-kordas-hoc Apr 16, 2026
d6a316c
Merge branch 'develop' into feature/hf-122-framework-integration-guides
sequba Apr 16, 2026
79f96e1
Docs: correct Next.js App Router SSR pattern in React guide
claude Apr 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions AGENTS.md
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
2 changes: 1 addition & 1 deletion docs/guide/custom-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@ it('returns a VALUE error if the range argument contains a string', () => {

## Working demo

Explore the full working example on [Stackblitz](https://stackblitz.com/github/handsontable/hyperformula-demos/tree/3.2.x/custom-functions?v=${$page.buildDateURIEncoded}).
Explore the full working example on <a :href="'https://stackblitz.com/github/handsontable/hyperformula-demos/tree/3.2.x/custom-functions?v=' + $page.buildDateURIEncoded">Stackblitz</a>.

This demo contains the implementation of both the
[`GREET`](#add-a-simple-custom-function) and
Expand Down
119 changes: 116 additions & 3 deletions docs/guide/integration-with-angular.md
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.

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>.
117 changes: 114 additions & 3 deletions docs/guide/integration-with-react.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,120 @@
# 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.

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 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 itself runs in Node.js, and the component above is already safe under SSR: the engine is constructed inside `useEffect`, which runs only on the client, and the initial render emits no cell values. You only need to take extra steps if you want to keep the HyperFormula bundle out of the server-rendered HTML, in which case lazy-load the component with `next/dynamic` and `ssr: false`.

In the App Router, `ssr: false` is not allowed inside a Server Component, so declare the component with `'use client'` and create a small client-side wrapper that performs the dynamic import:

```tsx
// app/spreadsheet/SpreadsheetComponent.tsx
'use client';
// ... component definition as above
```

```tsx
// app/spreadsheet/SpreadsheetLazy.tsx ← client component that owns the dynamic import
'use client';
import dynamic from 'next/dynamic';

export const SpreadsheetLazy = dynamic(
() => import('./SpreadsheetComponent').then((m) => m.SpreadsheetComponent),
{ ssr: false }
);
```

```tsx
// app/spreadsheet/page.tsx ← server component, no 'use client'
import { SpreadsheetLazy } from './SpreadsheetLazy';

export default function Page() {
return <SpreadsheetLazy />;
}
```

Keeping `dynamic(..., { ssr: false })` in a dedicated client wrapper preserves the server component status of `page.tsx` so only the spreadsheet widget hydrates on the client. In the Pages Router, the same `dynamic(..., { ssr: false })` call can live directly in the page file — the App Router's Server Component restriction does not apply there.

## 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>.
Loading
Loading