Skip to content

Commit 456addd

Browse files
marcin-kordas-hocsequbaclaude
authored
HF-122: Framework integration guides for React, Angular, Vue, Svelte (#1653)
## Summary Expand the four framework integration pages (React, Angular, Vue, Svelte) from one-line redirects into self-contained guides with code snippets extracted from the respective Stackblitz demos. Each guide's primary snippet is a simplified version of the demo's framework pattern — same lifecycle hooks, same service architecture, same reactivity approach — with simplified data (`buildFromArray` instead of Employee Table). ## Design rationale ### Snippets from demos, not invented patterns Per review feedback: every code snippet must match what's in the corresponding Stackblitz demo. This ensures the snippets are tested, idiomatic, and consistent with what users see when they click the demo link. Patterns not present in demos (e.g., Angular Signals, Svelte 5 runes) are deliberately excluded until validated by a framework expert. | Framework | Demo file | Primary pattern in guide | |---|---|---| | React | `react-demo/src/lib/employee/employee.provider.tsx` | `useRef` + `useEffect` init/cleanup + `useState` | | Angular | `angular-demo/src/app/employees/employees.service.ts` | `@Injectable` + `BehaviorSubject` + `async` pipe | | Vue | `vue-3-demo/src/lib/employees-data-provider.ts` | Class wrapper with private HF field + `ref` | | Svelte | `svelte-demo/src/routes/Hyperformula.svelte` | `buildFromArray` + `getCellValue` + `on:click` + `onDestroy` | ### Other decisions - **TypeScript** in all snippets (HF ships `.d.ts` typings) - **`licenseKey: 'gpl-v3'`** in every snippet (without it, engine throws license warning) - **SSR notes** for Next.js, Nuxt, SvelteKit (HF is SSR-safe — no browser-only API dependency — but instantiating it server-side is wasted work, so each framework's SSR section defers to client lifecycle) - **VuePress template fix** — Stackblitz links use `<a :href>` Vue binding instead of `{{ }}` interpolation in markdown ## Test plan - [x] Render docs locally / verify all four integration pages — all 4 pages return HTTP 200 on the Netlify deploy preview for the latest commit (proxies `npm run docs:dev`) - [x] Click each Stackblitz demo link — all 5 URLs (4 frameworks + custom-functions) reachable, each `hyperformula-demos@3.2.x/<framework>-demo` subdir exists - [x] Verify primary snippets match demo patterns — React: `useRef`/`useEffect`/`useState`; Angular: `@Injectable`/`BehaviorSubject`/`async` pipe; Vue: class wrapper + `ref` (the `markRaw` pattern is documented in Troubleshooting, not the primary snippet); Svelte: `buildFromArray`/`getCellValue`/`on:click`/`onDestroy` - [x] Verify no untested patterns remain — no Signals, no `$state`/`$derived` runes, no NgZone; Pinia is mentioned only in a Vue Troubleshooting note that warns against putting the engine into Pinia state, not as a recommended pattern - [x] Confirm `licenseKey: 'gpl-v3'` present in every snippet — react/angular: 1× (main snippet); vue: 2× (main + Troubleshooting markRaw demo); svelte: 2× (basic + SSR variants) - [x] Confirm `destroy()` cleanup present in every applicable component snippet — react/angular: 1× (main snippet); vue: 1× (main snippet — Troubleshooting markRaw demo is illustrative, not a full component); svelte: 2× (basic + SSR variants) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk documentation-only change that adds new framework-specific guidance and code snippets; no runtime/library behavior is modified. > > **Overview** > **Expands the framework integration docs** (Angular, React, Svelte, Vue) from brief install notes into self-contained guides with concrete TypeScript-centric examples for initializing HyperFormula, surfacing calculated values in each framework’s reactivity model, and cleaning up via the appropriate lifecycle hook. > > Adds SSR-specific notes for Angular Universal, Next.js, Nuxt, and SvelteKit, and standardizes demo links by switching Stackblitz URLs to Vue-bound `<a :href>` so the cache-busting query param renders correctly in VuePress (also applied to `custom-functions`). > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 1ecce54. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Kuba Sekowski <jakub.sekowski@handsontable.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 362df83 commit 456addd

5 files changed

Lines changed: 442 additions & 17 deletions

File tree

docs/guide/custom-functions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,7 @@ it('returns a VALUE error if the range argument contains a string', () => {
358358
359359
## Working demo
360360
361-
Explore the full working example on [Stackblitz](https://stackblitz.com/github/handsontable/hyperformula-demos/tree/3.2.x/custom-functions?v=${$page.buildDateURIEncoded}).
361+
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>.
362362
363363
This demo contains the implementation of both the
364364
[`GREET`](#add-a-simple-custom-function) and
Lines changed: 121 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,127 @@
11
# Integration with Angular
22

3-
Installing HyperFormula in an Angular application works the same as with vanilla JavaScript.
3+
The HyperFormula API is identical in an Angular app and in plain JavaScript. This guide demonstrates how HyperFormula is integrated with an Angular app (typically as an injectable service), how it is cleaned up, and how you bridge its values into the change-detection cycle.
44

5-
For more details, see the [client-side installation](client-side-installation.md) section.
5+
Install with `npm install hyperformula`. For other options, see the [client-side installation](client-side-installation.md) section.
6+
7+
## Basic usage
8+
9+
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.
10+
11+
```typescript
12+
// spreadsheet.service.ts
13+
import { Injectable } from '@angular/core';
14+
import { BehaviorSubject } from 'rxjs';
15+
import { HyperFormula, type CellValue } from 'hyperformula';
16+
17+
@Injectable({ providedIn: 'root' })
18+
export class SpreadsheetService {
19+
private readonly hf: HyperFormula;
20+
21+
private readonly _values = new BehaviorSubject<CellValue[][]>([]);
22+
readonly values$ = this._values.asObservable();
23+
24+
constructor() {
25+
this.hf = HyperFormula.buildFromArray(
26+
[
27+
[1, 2, '=A1+B1'],
28+
// your data rows go here
29+
],
30+
{
31+
licenseKey: 'gpl-v3',
32+
// more configuration options go here
33+
}
34+
);
35+
this._values.next(this.hf.getSheetValues(0));
36+
}
37+
38+
calculate() {
39+
this._values.next(this.hf.getSheetValues(0));
40+
}
41+
42+
reset() {
43+
this._values.next([]);
44+
}
45+
}
46+
```
47+
48+
Consume the service from a component and bind `values$ | async` in the template. Declare the component in your `AppModule` alongside `CommonModule`:
49+
50+
```typescript
51+
// spreadsheet.component.ts
52+
import { Component } from '@angular/core';
53+
import { Observable } from 'rxjs';
54+
import { SpreadsheetService } from './spreadsheet.service';
55+
import { type CellValue } from 'hyperformula';
56+
57+
@Component({
58+
selector: 'app-spreadsheet',
59+
templateUrl: './spreadsheet.component.html',
60+
})
61+
export class SpreadsheetComponent {
62+
values$: Observable<CellValue[][]>;
63+
64+
constructor(private spreadsheetService: SpreadsheetService) {
65+
this.values$ = this.spreadsheetService.values$;
66+
}
67+
68+
runCalculations() {
69+
this.spreadsheetService.calculate();
70+
}
71+
72+
reset() {
73+
this.spreadsheetService.reset();
74+
}
75+
}
76+
```
77+
78+
```html
79+
<!-- spreadsheet.component.html -->
80+
<button (click)="runCalculations()">Run calculations</button>
81+
<button (click)="reset()">Reset</button>
82+
<ng-container *ngIf="(values$ | async) as values">
83+
<table *ngIf="values.length">
84+
<tr *ngFor="let row of values">
85+
<td *ngFor="let cell of row">{{ cell }}</td>
86+
</tr>
87+
</table>
88+
</ng-container>
89+
```
90+
91+
## Notes
92+
93+
### Provider scope
94+
95+
`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.
96+
97+
### Cleanup
98+
99+
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:
100+
101+
```typescript
102+
import { Injectable, OnDestroy } from '@angular/core';
103+
104+
@Injectable()
105+
export class SpreadsheetService implements OnDestroy {
106+
// ...
107+
108+
ngOnDestroy() {
109+
this.hf.destroy();
110+
}
111+
}
112+
```
113+
114+
## Server-side rendering (Angular Universal)
115+
116+
The service above is already SSR-safe — HyperFormula has no browser-only API dependency. To skip the (otherwise wasted) server-side instantiation in Angular Universal, gate the engine init with [`isPlatformBrowser`](https://angular.dev/api/common/isPlatformBrowser) from `@angular/common`.
117+
118+
## Next steps
119+
120+
- [Configuration options](configuration-options.md) — full list of `buildFromArray` / `buildEmpty` options
121+
- [Basic operations](basic-operations.md) — CRUD on cells, rows, columns, sheets
122+
- [Advanced usage](advanced-usage.md) — multi-sheet workbooks, named expressions
123+
- [Custom functions](custom-functions.md) — register your own formulas
6124

7125
## Demo
8126

9-
Explore the full working example on [Stackblitz](https://stackblitz.com/github/handsontable/hyperformula-demos/tree/3.2.x/angular-demo?v=${$page.buildDateURIEncoded}).
127+
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>.
Lines changed: 111 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,117 @@
11
# Integration with React
22

3-
Installing HyperFormula in a React application works the same as with vanilla JavaScript.
3+
The HyperFormula API is identical in a React app and in plain JavaScript. This guide demonstrates how HyperFormula is integrated with the React component tree and how its lifecycle maps to React hooks.
44

5-
For more details, see the [client-side installation](client-side-installation.md) section.
5+
Install with `npm install hyperformula`. For other options, see the [client-side installation](client-side-installation.md) section.
6+
7+
## Basic usage
8+
9+
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.
10+
11+
```tsx
12+
import { useEffect, useRef, useState } from 'react';
13+
import { HyperFormula } from 'hyperformula';
14+
import type { CellValue } from 'hyperformula';
15+
16+
export default function SpreadsheetComponent() {
17+
const hfRef = useRef<HyperFormula | null>(null);
18+
const [values, setValues] = useState<CellValue[][]>([]);
19+
20+
useEffect(() => {
21+
const hf = HyperFormula.buildFromArray(
22+
[
23+
[1, 2, '=A1+B1'],
24+
// your data rows go here
25+
],
26+
{
27+
licenseKey: 'gpl-v3',
28+
// more configuration options go here
29+
}
30+
);
31+
hfRef.current = hf;
32+
33+
return () => {
34+
hf.destroy();
35+
hfRef.current = null;
36+
};
37+
}, []);
38+
39+
function runCalculations() {
40+
if (!hfRef.current) return;
41+
setValues(hfRef.current.getSheetValues(0));
42+
}
43+
44+
function reset() {
45+
setValues([]);
46+
}
47+
48+
return (
49+
<>
50+
<button onClick={runCalculations}>Run calculations</button>
51+
<button onClick={reset}>Reset</button>
52+
{values.length > 0 && (
53+
<table>
54+
<tbody>
55+
{values.map((row, r) => (
56+
<tr key={r}>
57+
{row.map((cell, c) => (
58+
<td key={c}>{String(cell ?? '')}</td>
59+
))}
60+
</tr>
61+
))}
62+
</tbody>
63+
</table>
64+
)}
65+
</>
66+
);
67+
}
68+
```
69+
70+
If you use JavaScript instead of TypeScript, drop the type annotations — the rest of the pattern is unchanged.
71+
72+
## `React.StrictMode` double invocation
73+
74+
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.
75+
76+
## Server-side rendering (Next.js App Router)
77+
78+
The component above is already SSR-safe — the engine is constructed in `useEffect`, which never runs on the server. If you still want to skip the initial bundle on the server (it is a few hundred kB), wrap it in a client-only dynamic import.
79+
80+
In the App Router, `dynamic(..., { ssr: false })` is only allowed inside a client component. Put the dynamic call in a `'use client'` wrapper and import the wrapper from your server page:
81+
82+
```tsx
83+
// app/spreadsheet/SpreadsheetLazy.tsx
84+
'use client';
85+
import dynamic from 'next/dynamic';
86+
87+
const SpreadsheetComponent = dynamic(
88+
() => import('./SpreadsheetComponent'),
89+
{ ssr: false }
90+
);
91+
92+
export default function SpreadsheetLazy() {
93+
return <SpreadsheetComponent />;
94+
}
95+
```
96+
97+
```tsx
98+
// app/spreadsheet/page.tsx ← server component, no 'use client'
99+
import SpreadsheetLazy from './SpreadsheetLazy';
100+
101+
export default function Page() {
102+
return <SpreadsheetLazy />;
103+
}
104+
```
105+
106+
In the Pages Router, the same `dynamic(..., { ssr: false })` call works directly in the page file without a wrapper.
107+
108+
## Next steps
109+
110+
- [Configuration options](configuration-options.md) — full list of `buildFromArray` / `buildEmpty` options
111+
- [Basic operations](basic-operations.md) — CRUD on cells, rows, columns, sheets
112+
- [Advanced usage](advanced-usage.md) — multi-sheet workbooks, named expressions
113+
- [Custom functions](custom-functions.md) — register your own formulas
6114

7115
## Demo
8116

9-
Explore the full working example on [Stackblitz](https://stackblitz.com/github/handsontable/hyperformula-demos/tree/3.2.x/react-demo?v=${$page.buildDateURIEncoded}).
117+
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>.
Lines changed: 120 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,126 @@
11
# Integration with Svelte
22

3-
Installing HyperFormula in a Svelte application works the same as with vanilla JavaScript.
3+
The HyperFormula API is identical in a Svelte app and in plain JavaScript. This guide demonstrates how HyperFormula integrates with the Svelte component's lifecycle and how you bridge its values into Svelte's reactivity.
44

5-
For more details, see the [client-side installation](client-side-installation.md) section.
5+
Install with `npm install hyperformula`. For other options, see the [client-side installation](client-side-installation.md) section.
6+
7+
::: warning SvelteKit SSR
8+
The primary snippet below assumes a browser environment. If you use SvelteKit with default SSR, skip to [Server-side rendering](#server-side-rendering-sveltekit)`HyperFormula.buildFromArray` at `<script>` top level will run on every server render, which is unnecessary work.
9+
:::
10+
11+
## Basic usage
12+
13+
Declare the engine at the top of `<script>` so it lives for the component's lifetime. Call `getCellValue` on demand and display results in the template. Release the engine with `onDestroy`.
14+
15+
```html
16+
<script>
17+
import { onDestroy } from 'svelte';
18+
import { HyperFormula } from 'hyperformula';
19+
20+
const data = [
21+
[1, 2, '=A1+B1'],
22+
// your data rows go here
23+
];
24+
25+
const hf = HyperFormula.buildFromArray(data, {
26+
licenseKey: 'gpl-v3',
27+
// more configuration options go here
28+
});
29+
30+
const sheetId = 0;
31+
/** @type {import('hyperformula').CellValue} */
32+
let result = null;
33+
34+
function calculate() {
35+
result = hf.getCellValue({ sheet: sheetId, row: 0, col: 2 });
36+
}
37+
38+
function reset() {
39+
result = null;
40+
}
41+
42+
onDestroy(() => hf.destroy());
43+
</script>
44+
45+
<button on:click={calculate}>Run calculations</button>
46+
<button on:click={reset}>Reset</button>
47+
{#if result !== null}
48+
<p>Result: <strong>{result}</strong></p>
49+
{/if}
50+
51+
<table>
52+
<tbody>
53+
{#each data as row, r}
54+
<tr>
55+
{#each row as cell, c}
56+
<td>
57+
{#if hf.doesCellHaveFormula({ sheet: sheetId, row: r, col: c })}
58+
{hf.getCellFormula({ sheet: sheetId, row: r, col: c })}
59+
{:else}
60+
{hf.getCellValue({ sheet: sheetId, row: r, col: c })}
61+
{/if}
62+
</td>
63+
{/each}
64+
</tr>
65+
{/each}
66+
</tbody>
67+
</table>
68+
```
69+
70+
## Server-side rendering (SvelteKit)
71+
72+
In SvelteKit, top-level statements in `<script>` run on the server too. HyperFormula has no browser-only API dependency, but instantiating it during SSR is wasted work. Move the initialization into `onMount` so it only runs on the client:
73+
74+
`onMount` is allowed to be `async`, but any cleanup function returned from an async callback is silently ignored — an async function always returns a `Promise`, not the cleanup. Put the teardown in a separate `onDestroy` instead:
75+
76+
```html
77+
<script>
78+
// Svelte 4 + SvelteKit
79+
import { onDestroy, onMount } from 'svelte';
80+
81+
let hf;
82+
/** @type {import('hyperformula').CellValue} */
83+
let result = null;
84+
85+
onMount(async () => {
86+
const { HyperFormula } = await import('hyperformula');
87+
hf = HyperFormula.buildFromArray(
88+
[
89+
[1, 2, '=A1+B1'],
90+
// your data rows go here
91+
],
92+
{ licenseKey: 'gpl-v3' }
93+
);
94+
});
95+
96+
// Separate onDestroy — async onMount cannot return a cleanup.
97+
onDestroy(() => hf?.destroy());
98+
99+
function calculate() {
100+
if (!hf) return;
101+
result = hf.getCellValue({ sheet: 0, row: 0, col: 2 });
102+
}
103+
104+
function reset() {
105+
result = null;
106+
}
107+
</script>
108+
109+
<button on:click={calculate}>Run calculations</button>
110+
<button on:click={reset}>Reset</button>
111+
{#if result !== null}
112+
<p>Result: <strong>{result}</strong></p>
113+
{/if}
114+
```
115+
116+
117+
## Next steps
118+
119+
- [Configuration options](configuration-options.md) — full list of `buildFromArray` / `buildEmpty` options
120+
- [Basic operations](basic-operations.md) — CRUD on cells, rows, columns, sheets
121+
- [Advanced usage](advanced-usage.md) — multi-sheet workbooks, named expressions
122+
- [Custom functions](custom-functions.md) — register your own formulas
6123
7124
## Demo
8125
9-
Explore the full working example on [Stackblitz](https://stackblitz.com/github/handsontable/hyperformula-demos/tree/3.2.x/svelte-demo?v=${$page.buildDateURIEncoded}).
126+
For a more advanced example, check out the <a :href="'https://stackblitz.com/github/handsontable/hyperformula-demos/tree/3.2.x/svelte-demo?v=' + $page.buildDateURIEncoded">Svelte demo on Stackblitz</a>.

0 commit comments

Comments
 (0)