diff --git a/FUNCTIONAL_COMPONENT_GENERATION_GUIDE.md b/FUNCTIONAL_COMPONENT_GENERATION_GUIDE.md new file mode 100644 index 0000000000..8da661e2fa --- /dev/null +++ b/FUNCTIONAL_COMPONENT_GENERATION_GUIDE.md @@ -0,0 +1,1038 @@ +# Functional Component Generation Guide + +This guide is the authoritative reference for **generating** Ignite UI React functional component samples programmatically. Every construct a generated sample may need is covered below with: + +- **When to use it** — the trigger condition in the source class +- **Class template** — the exact pattern that appears in class-based originals +- **Functional template** — the exact generated output +- **Rules** — edge cases, ordering requirements, and common mistakes to avoid + +Companion document: [`FUNCTIONAL_REFACTORING_README.md`](./FUNCTIONAL_REFACTORING_README.md) — a sample-by-sample mapping of what was refactored and why. + +--- + +## Table of Contents + +1. [File skeleton](#1-file-skeleton) +2. [Imports](#2-imports) +3. [Module registration](#3-module-registration) +4. [State](#4-state) +5. [Refs — component or DOM element accessed imperatively](#5-refs--component-or-dom-element-accessed-imperatively) +6. [Cross-component wiring — instance as a prop](#6-cross-component-wiring--instance-as-a-prop) +7. [Cross-component wiring — imperative assignment after render](#7-cross-component-wiring--imperative-assignment-after-render) +8. [Lazy getters and data sources](#8-lazy-getters-and-data-sources) +9. [ComponentRenderer](#9-componentrenderer) +10. [Event handlers](#10-event-handlers) +11. [componentDidMount (one-time setup)](#11-componentdidmount-one-time-setup) +12. [componentWillUnmount (cleanup)](#12-componentwillunmount-cleanup) +13. [setInterval management](#13-setinterval-management) +14. [Async data loading](#14-async-data-loading) +15. [Native / web-component event listeners](#15-native--web-component-event-listeners) +16. [Static or inline data](#16-static-or-inline-data) +17. [Pure helper functions](#17-pure-helper-functions) +18. [Aggregate functions (Pivot Grid)](#18-aggregate-functions-pivot-grid) +19. [One-time side effects (icon registration)](#19-one-time-side-effects-icon-registration) +20. [mountedRef guard (prevent post-unmount updates)](#20-mountedref-guard-prevent-post-unmount-updates) +21. [Hook import checklist](#21-hook-import-checklist) +22. [Complete generated skeleton](#22-complete-generated-skeleton) + +--- + +## 1. File skeleton + +### Class template +```tsx +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; +// ... more imports + +export default class Sample extends React.Component { + + constructor(props: any) { + super(props); + // state init, .bind() calls + } + + public render(): JSX.Element { + return ( /* JSX */ ); + } +} + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render(); +``` + +### Functional template +```tsx +import React, { /* only the hooks you need */ } from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; +// ... same component imports + +// Module-level registrations, constants, pure functions go here +// (see sections 3, 16, 17, 18, 19) + +export default function Sample() { + // hooks go here (useState, useRef, useMemo, useCallback, useEffect) + + return ( /* same JSX, `this.` removed */ ); +} + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render(); +``` + +**Rules** +- The component name stays the same. +- The `ReactDOM.createRoot` line is identical. +- `constructor`, `super(props)`, and all `.bind()` calls are deleted entirely. + +--- + +## 2. Imports + +### Class template +```tsx +import React from 'react'; +``` + +### Functional template +```tsx +import React, { useState, useRef, useMemo, useCallback, useEffect } from 'react'; +``` + +**Rules** +- Only import the hooks that are actually used (see [Section 21](#21-hook-import-checklist) for a decision tree). +- Do **not** import hooks that are not needed — unused imports generate lint warnings. + +--- + +## 3. Module registration + +Module registration already lives at module scope in most originals. No change is needed — keep it identical. + +### Class template +```tsx +const mods: any[] = [ + IgrDataPieChartModule, + IgrItemLegendModule, +]; +mods.forEach((m) => m.register()); +``` + +or + +```tsx +IgrFinancialChartModule.register(); +IgrLegendModule.register(); +``` + +### Functional template +```tsx +// identical — stays at module scope +const mods: any[] = [ + IgrDataPieChartModule, + IgrItemLegendModule, +]; +mods.forEach(m => m.register()); +``` + +**Rules** +- Never move registration inside the component function body — it would re-run on every render. +- The `(m) => m.register()` arrow can optionally be shortened to `m => m.register()`. + +--- + +## 4. State + +**Trigger:** the class has `this.state = { ... }` in the constructor and `this.setState(...)` calls in methods. + +### Class template +```tsx +constructor(props: any) { + super(props); + this.state = { + chartType: 'Auto', + isLoading: false, + items: [], + }; +} + +// inside a method: +this.setState({ chartType: 'Bar' }); +this.setState({ isLoading: true, items: [] }); // multiple keys at once +``` + +### Functional template +```tsx +// One useState per logical piece of state +const [chartType, setChartType] = useState('Auto'); +const [isLoading, setIsLoading] = useState(false); +const [items, setItems] = useState([]); + +// inside a handler: +setChartType('Bar'); +setIsLoading(true); +setItems([]); +``` + +**Rules** +- Split state into independent pieces; do **not** put everything in one `useState({})` object unless the fields are always updated together. +- When multiple related fields are always updated atomically (e.g. `{ dataInfo, dataPoints }`), a single `useState` object with a spread-update is acceptable: + ```tsx + const [info, setInfo] = useState({ dataInfo: '500', dataPoints: 500 }); + // update: + setInfo(prev => ({ ...prev, dataInfo: '1K', dataPoints: 1000 })); + ``` +- `this.setState` merges keys; the functional setter **replaces** the value — always spread when updating an object state. + +--- + +## 5. Refs — component or DOM element accessed imperatively + +**Trigger:** the class stores an Ignite UI component or DOM element instance as a plain field (`public chart: IgrCategoryChart`) and accesses it via `this.chart.someMethod()`. + +### Class template +```tsx +public chart: IgrCategoryChart; + +constructor(props: any) { + super(props); + this.onChartRef = this.onChartRef.bind(this); +} + +public onChartRef(chart: IgrCategoryChart) { + if (!chart) { return; } + this.chart = chart; +} + +// in render(): + + +// later usage: +this.chart.replayTransitionIn(); +``` + +### Functional template +```tsx +const chartRef = useRef(null); + +// in JSX: + + +// later usage: +chartRef.current?.replayTransitionIn(); +``` + +**Rules** +- Use `useRef` (not `useState`) because reading the ref never needs to trigger a re-render. +- Pass the ref object directly as the `ref` prop — React sets `.current` to the component instance on mount and back to `null` on unmount automatically. +- Access the element as `chartRef.current` (may be `null` — use optional chaining `?.`). +- **Do not** call `this.setState({})` after assigning the ref — the `useRef` equivalent never triggers re-renders. +- **Do not** wrap in a `useCallback` callback ref just to assign `chartRef.current` — that is redundant. Only use a callback ref (see Section 13) when the mounting event itself must trigger additional side effects such as starting an interval. + +--- + +## 6. Cross-component wiring — instance as a prop + +**Trigger:** a callback ref calls `this.setState({})` purely to trigger a re-render, so that a newly-available component instance (e.g. a legend) can be passed as a **prop** to another component (`legend={this.legend}`). + +### Class template +```tsx +private legend: IgrItemLegend; +private legendRef(r: IgrItemLegend) { + this.legend = r; + this.setState({}); // force re-render so legend becomes available as a prop +} +private chart: IgrDataPieChart; +private chartRef(r: IgrDataPieChart) { + this.chart = r; + this.setState({}); +} + +constructor(props: any) { + super(props); + this.legendRef = this.legendRef.bind(this); + this.chartRef = this.chartRef.bind(this); +} + +// in render(): + + +``` + +### Functional template +```tsx +// Store the instance in STATE — the setter causes the re-render that delivers +// the instance as a prop on the next paint. +const [legend, setLegend] = useState(null); + +// in JSX: + { if (r) setLegend(r); }} + orientation="Horizontal" +/> + +``` + +**Rules** +- Use `useState`, not `useRef`, because the component instance must be available as a JSX prop — which requires a re-render to propagate. +- The inline ref callback `(r) => { if (r) setSomething(r); }` is intentional. Do **not** wrap it in `useCallback` — the identity change is harmless for ref callbacks and avoids extra complexity. +- Use `legend ?? undefined` to convert `null` (initial state) to `undefined` so the prop is simply absent until the legend mounts. +- A second `useState` for the chart instance is only needed if the chart ref is also used as a prop elsewhere. If only the legend needs wiring, only the legend needs `useState`. + +--- + +## 7. Cross-component wiring — imperative assignment after render + +**Trigger:** two callback refs call each other's instance imperatively (`this.chart.legend = this.legend`) rather than passing the instance as a prop. + +### Class template +```tsx +public chart: IgrFinancialChart; +public legend: IgrLegend; + +public onChartRef(chart: IgrFinancialChart) { + if (!chart) { return; } + this.chart = chart; + if (this.legend) { + this.chart.legend = this.legend; + } +} + +public onLegendRef(legend: IgrLegend) { + if (!legend) { return; } + this.legend = legend; + if (this.chart) { + this.chart.legend = this.legend; + } +} +``` + +### Functional template +```tsx +const chartRef = useRef(null); +const legendRef = useRef(null); + +// Runs after every render; wires the two together once both refs are populated. +// No dependency array — intentionally runs every render to catch late mounts. +useEffect(() => { + if (chartRef.current && legendRef.current) { + chartRef.current.legend = legendRef.current; + } +}); + +// in JSX: + + +``` + +**Rules** +- Omit the dependency array (`[]`) from `useEffect` so it runs after every render. This guarantees the wiring fires regardless of which ref becomes available first. +- Use `useRef` (not `useState`) because the assignment is imperative — nothing in JSX depends on the value, so no re-render is needed. +- This pattern is preferred when the component's API requires imperative property assignment (i.e. the property is not a declarative JSX prop). + +--- + +## 8. Lazy getters and data sources + +**Trigger:** a class has a private backing field (`_foo: FooClass = null`) and a public getter with a null-check that initialises on first access. + +### Class template +```tsx +private _energyGlobalDemand: EnergyGlobalDemand = null; +public get energyGlobalDemand(): EnergyGlobalDemand { + if (this._energyGlobalDemand == null) { + this._energyGlobalDemand = new EnergyGlobalDemand(); + } + return this._energyGlobalDemand; +} + +// used as: +dataSource={this.energyGlobalDemand} +``` + +### Functional template +```tsx +// useMemo with [] runs the factory exactly once — equivalent to the lazy getter. +const energyGlobalDemand = useMemo(() => new EnergyGlobalDemand(), []); + +// used as: +dataSource={energyGlobalDemand} +``` + +**Rules** +- Always pass `[]` as the dependency array — the data source should be created once and never recreated. +- `useMemo` does not guarantee permanent memoization in strict React internals, but in practice with an empty deps array it is stable for the component's lifetime. +- If the data source class takes constructor arguments that can change, list them in the deps array. + +--- + +## 9. ComponentRenderer + +**Trigger:** the class has a lazy getter for a `ComponentRenderer` instance that registers description modules via `context`. + +### Class template +```tsx +private _componentRenderer: ComponentRenderer = null; +public get renderer(): ComponentRenderer { + if (this._componentRenderer == null) { + this._componentRenderer = new ComponentRenderer(); + var context = this._componentRenderer.context; + PropertyEditorPanelDescriptionModule.register(context); + DataPieChartDescriptionModule.register(context); + ItemLegendDescriptionModule.register(context); + } + return this._componentRenderer; +} + +// used as: +componentRenderer={this.renderer} +``` + +### Functional template +```tsx +const renderer = useMemo(() => { + const r = new ComponentRenderer(); + const ctx = r.context; + PropertyEditorPanelDescriptionModule.register(ctx); + DataPieChartDescriptionModule.register(ctx); + ItemLegendDescriptionModule.register(ctx); + return r; +}, []); + +// used as: +componentRenderer={renderer} +``` + +**Rules** +- Use `useMemo(() => ..., [])` — identical reasoning to lazy getters in [Section 8](#8-lazy-getters-and-data-sources). +- Use `const ctx = r.context` (not `var context`) to stay consistent with modern TypeScript style. +- The list of `register` calls must be identical to the class version. + +--- + +## 10. Event handlers + +**Trigger:** the class has methods used as event callbacks, bound with `.bind(this)` in the constructor. + +### Class template +```tsx +constructor(props: any) { + super(props); + this.onTransitionInModeChanged = this.onTransitionInModeChanged.bind(this); + this.onReloadChartClick = this.onReloadChartClick.bind(this); +} + +public onTransitionInModeChanged(e: any) { + this.setState({ transitionInMode: e.target.value }); +} + +public onReloadChartClick() { + this.chart.replayTransitionIn(); +} + +// in render(): + + +``` + +### Functional template +```tsx +const onTransitionInModeChanged = useCallback((e: React.ChangeEvent) => { + setTransitionInMode(e.target.value); +}, []); + +const onReloadChartClick = useCallback(() => { + chartRef.current?.replayTransitionIn(); +}, []); + +// in JSX: + + +``` + +**Rules** +- Always wrap event handlers in `useCallback` to keep handler identity stable across renders (prevents unnecessary child re-renders). +- The dependency array `[]` is correct when the handler only reads from `useRef` values or `useState` setter functions (both are stable references). +- If the handler reads a `useState` value (not the setter), add it to the deps array, or use the functional updater form of the setter (`prev => ...`) to avoid stale closures. +- `this.` is removed from all internal usages; `this.chart.method()` becomes `chartRef.current?.method()`. + +--- + +## 11. componentDidMount (one-time setup) + +**Trigger:** the class has a `componentDidMount` method. + +### Class template +```tsx +public componentDidMount() { + this.setupSomething(); +} +``` + +### Functional template +```tsx +useEffect(() => { + setupSomething(); +}, []); // [] = run once after first render, equivalent to componentDidMount +``` + +**Rules** +- The empty dependency array `[]` is the exact functional equivalent of `componentDidMount`. +- If the setup relies on state or prop values that could change, list them in the array. +- Put the `useEffect` near the top of the component body, after all `useRef` / `useState` declarations, before the `return`. + +--- + +## 12. componentWillUnmount (cleanup) + +**Trigger:** the class has a `componentWillUnmount` method that tears down timers, listeners, etc. + +### Class template +```tsx +public componentWillUnmount() { + if (this.interval >= 0) { + window.clearInterval(this.interval); + this.interval = -1; + } +} +``` + +### Functional template +```tsx +useEffect(() => { + // setup ... + return () => { + // cleanup — runs on unmount (and before re-running on dep changes) + if (intervalRef.current >= 0) { + window.clearInterval(intervalRef.current); + intervalRef.current = -1; + } + }; +}, [/* same deps as setup */]); +``` + +**Rules** +- The cleanup function is **always the return value** of the same `useEffect` that performed the setup. This collocates setup and teardown, making it impossible for one to exist without the other. +- The cleanup runs both on unmount **and** before the effect re-runs when dependencies change — this is more correct than `componentWillUnmount` which only ran on unmount. + +--- + +## 13. setInterval management + +**Trigger:** the class stores an interval ID as a class field and manages it across multiple methods. + +### Class template +```tsx +public interval: number = -1; +public chart: IgrCategoryChart; +public data: any[]; +public dataIndex: number = 0; +public refreshMilliseconds: number = 10; + +public onChartRef(chart: IgrCategoryChart) { + this.chart = chart; + this.setupInterval(); +} + +public setupInterval(): void { + if (this.interval >= 0) { + window.clearInterval(this.interval); + } + this.interval = window.setInterval(() => this.tick(), this.refreshMilliseconds); +} + +public tick(): void { + // mutate this.data, call this.chart.notifyInsertItem(...) +} + +public componentWillUnmount() { + window.clearInterval(this.interval); +} +``` + +### Functional template +```tsx +// Mutable values that must survive re-renders but not trigger them live in refs +const chartRef = useRef(null); +const dataRef = useRef(initialData); +const dataIndexRef = useRef(0); +const refreshMsRef = useRef(10); +const intervalRef = useRef(-1); +const mountedRef = useRef(true); // guards against post-unmount updates + +const setupInterval = useCallback(() => { + if (intervalRef.current >= 0) { + window.clearInterval(intervalRef.current); + intervalRef.current = -1; + } + intervalRef.current = window.setInterval(() => { + if (!mountedRef.current) return; // component already unmounted — bail + setState(prev => { + // Read the latest state via `prev` to avoid stale closures. + // Mutate data refs and call imperative chart API here. + // Return `prev` unchanged if no state update is needed. + return prev; + }); + }, refreshMsRef.current); +}, []); + +// Wire chart ref → start interval; cleanup on unmount +const onChartRef = useCallback((chart: IgrCategoryChart) => { + if (!chart) return; + chartRef.current = chart; + setupInterval(); +}, [setupInterval]); + +useEffect(() => { + return () => { + mountedRef.current = false; + if (intervalRef.current >= 0) { + window.clearInterval(intervalRef.current); + intervalRef.current = -1; + } + }; +}, []); + +// Usage in JSX: +// +``` + +**Rules** +- **Never** store the interval ID in `useState` — this would trigger a re-render every time the interval is created or cleared. +- **Never** read `useState` values directly inside the interval callback (stale closure). Instead: use the functional updater `setState(prev => ...)` to read the latest state, or store the value in a ref (`refreshMsRef.current`). +- Use `mountedRef` to guard against calling `setState` on an unmounted component (React 18 no longer warns, but it can still cause logic errors). +- When `refreshMilliseconds` changes, call `setupInterval()` again — the new interval replaces the old one. + +--- + +## 14. Async data loading + +**Trigger:** the class calls an async data-fetching method in the constructor (or `componentDidMount`) and stores the result in state. + +### Class template +```tsx +constructor(props: any) { + super(props); + this.state = { data: [] }; + this.initData(); +} + +public initData() { + SomeService.getData().then((result: any[]) => { + this.setState({ data: result }); + }); +} +``` + +### Functional template +```tsx +const [data, setData] = useState([]); + +useEffect(() => { + SomeService.getData().then((result: any[]) => { + setData(result); + }); +}, []); // [] = fetch once on mount +``` + +**Rules** +- The `[]` dependency array runs the fetch exactly once — equivalent to a constructor-time call. +- If you need to cancel the fetch on unmount (e.g. `AbortController`), return a cleanup function from the effect. +- Do **not** call `initData()` or equivalent from the constructor — functional components have no constructor. + +--- + +## 15. Native / web-component event listeners + +**Trigger:** the class uses `componentDidMount` to attach `addEventListener` calls to DOM elements or refs (common with Ignite UI web components that emit `igcInput`, `igcChange`, etc.). + +### Class template +```tsx +public componentDidMount() { + this.infoForm.addEventListener('igcInput', this.onInput); + this.addressForm.addEventListener('igcInput', this.onInput); +} + +public componentWillUnmount() { + this.infoForm?.removeEventListener('igcInput', this.onInput); + this.addressForm?.removeEventListener('igcInput', this.onInput); +} +``` + +### Functional template +```tsx +const infoFormRef = useRef(null); +const addressFormRef = useRef(null); + +const handleInput = useCallback( + () => checkActiveStepValidity(linear), + [linear, checkActiveStepValidity] +); + +// In JSX – attach via React's onInput prop so no manual listener management is needed: +
...
+
...
+``` + +**Rules** +- Prefer React's synthetic `onInput` / `onChange` props on native `
` (or other container) elements instead of manually calling `addEventListener` inside a `useEffect`. Ignite UI web components fire standard DOM `input` / `change` events that bubble up through the shadow DOM, so they are caught by the parent ``'s React event handler automatically. +- Define the handler as a `useCallback` at component scope with the relevant state values in its dependency array so it always closes over the latest values. +- Only fall back to `useEffect` + `addEventListener` when the target element is not rendered by React (e.g. a third-party widget mounted outside the React tree) or when you need to attach to a non-standard event name that has no React equivalent. + +--- + +## 16. Static or inline data + +**Trigger:** the class has an `initData()` or equivalent method that builds a static (never-changing) data array and stores it in `this.state` or a class field. + +### Class template +```tsx +public data: any[]; + +constructor(props: any) { + super(props); + this.initData(); +} + +public initData() { + this.data = [ + { Year: '2009', Europe: 31, China: 21, USA: 19 }, + { Year: '2010', Europe: 43, China: 26, USA: 24 }, + // ... + ]; +} + +// in render(): +dataSource={this.data} +``` + +### Functional template +```tsx +// Moved to module scope — created once per module load +const CHART_DATA = [ + { Year: '2009', Europe: 31, China: 21, USA: 19 }, + { Year: '2010', Europe: 43, China: 26, USA: 24 }, + // ... +]; + +// in JSX: +dataSource={CHART_DATA} +``` + +**Rules** +- Move static data **outside** the component function. Inside the function, it would be recreated on every render. +- Name constants with `SCREAMING_SNAKE_CASE` to signal that they are module-level constants. +- If the data array is built from props or state, keep it inside the component and use `useMemo`. + +--- + +## 17. Pure helper functions + +**Trigger:** the class has instance methods that are pure (no `this.*` usage beyond what can be passed as arguments), used as tooltip renderers, formatters, or utility callbacks. + +### Class template +```tsx +public createTooltip(series: IgrGeographicMapSeries, item: any): string { + return `${item.name}
Lat: ${item.lat}
Lon: ${item.lon}`; +} + +// used as: +tooltipTemplate={this.createTooltip} +``` + +### Functional template +```tsx +// Moved to module scope — no `this`, no closure over component state +function createTooltip(series: IgrGeographicMapSeries, item: any): string { + return `${item.name}
Lat: ${item.lat}
Lon: ${item.lon}`; +} + +// used as (reference is stable, no binding needed): +tooltipTemplate={createTooltip} +``` + +**Rules** +- If the function does **not** read any state, props, or refs — move it to module scope. +- If it does read component state, wrap it in `useCallback` instead. +- Module-level functions have stable identity (never change reference), so passing them as event handler props is efficient. + +--- + +## 18. Aggregate functions (Pivot Grid) + +**Trigger:** the class has public methods used as Ignite UI `IgrPivotAggregator` implementations. + +### Class template +```tsx +public weightedAvg( + members: any[], + data: any[], + allData: any[], + fieldName: string, + pivotDimension: IgrPivotDimension +): any { + // aggregate logic +} + +// used as: +aggregatorName: IgrPivotNumericAggregate.sum.name, +// or wired via configuration object referencing `this.weightedAvg` +``` + +### Functional template +```tsx +// Promoted to module scope (pure functions — no `this`) +function weightedAvg( + members: any[], + data: any[], + allData: any[], + fieldName: string, + pivotDimension: IgrPivotDimension +): any { + // identical aggregate logic +} + +// used exactly the same way in the configuration object +``` + +**Rules** +- Identical to [Section 17](#17-pure-helper-functions) — move to module scope. +- The function signature must remain identical; do not rename parameters. + +--- + +## 19. One-time side effects (icon registration) + +**Trigger:** the class calls `registerIconFromText(...)` or similar one-time registrations inside the constructor. + +### Class template +```tsx +constructor(props: any) { + super(props); + registerIconFromText('add', ADD_SVG, 'material'); + registerIconFromText('delete', DELETE_SVG, 'material'); +} +``` + +### Functional template +```tsx +// Moved to module scope — runs once when the module is first imported +registerIconFromText('add', ADD_SVG, 'material'); +registerIconFromText('delete', DELETE_SVG, 'material'); + +export default function Sample() { + // no registration here +} +``` + +**Rules** +- Moving registrations to module scope means they run exactly once per module load, not once per component instantiation — which is the correct semantic. +- Do **not** put them inside `useEffect` unless the registration genuinely depends on the DOM (rare). Module-scope execution is earlier and simpler. + +--- + +## 20. mountedRef guard (prevent post-unmount updates) + +**When to use:** any component that has async operations (fetch, setInterval, setTimeout) that call state setters. A component may unmount before the async operation completes, causing a "cannot update state on an unmounted component" style issue. + +### Functional template +```tsx +const mountedRef = useRef(true); + +useEffect(() => { + return () => { + mountedRef.current = false; + }; +}, []); + +// Inside async callbacks or interval ticks: +if (!mountedRef.current) return; +setState(...); +``` + +**Rules** +- Always declare `mountedRef` immediately before the first `useEffect` that starts async work. +- Check `mountedRef.current` at the top of every async callback before calling any state setter. +- This ref **never** needs to be in a dependency array — it is intentionally mutable outside React's tracking. + +--- + +## 21. Hook import checklist + +Use this table to decide which hooks to import: + +| Situation | Hook | +|---|---| +| Component has `this.state = {...}` / `this.setState(...)` | `useState` | +| Component stores a component/DOM ref on `this` (accessed imperatively) | `useRef` | +| Component stores mutable non-render values on `this` (interval ID, counter, array) | `useRef` | +| Component uses a `mountedRef` guard | `useRef` | +| Component has a lazy getter with backing field | `useMemo` | +| Component has a `ComponentRenderer` lazy getter | `useMemo` | +| Component has static data inside `initData()` used as a prop | module-level `const` (no hook needed) | +| Component has `componentDidMount` or `componentWillUnmount` | `useEffect` | +| Component manages a `setInterval` / `setTimeout` | `useEffect` + `useRef` | +| Component loads async data | `useEffect` | +| Component attaches native event listeners | `useEffect` | +| Component needs cross-component wiring via a prop | `useState` | +| Component needs cross-component wiring via imperative assignment | `useRef` + `useEffect` | +| Component has event handler methods bound in constructor | `useCallback` | + +--- + +## 22. Complete generated skeleton + +This is the full template to start from. Delete sections that do not apply. + +```tsx +import React, { useState, useRef, useMemo, useCallback, useEffect } from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; + +// ── Ignite UI imports (same as class original) ──────────────────────────────── +import { IgrSomeComponentModule } from 'igniteui-react-charts'; +import { IgrSomeComponent } from 'igniteui-react-charts'; +import { IgrItemLegend, IgrItemLegendModule } from 'igniteui-react-charts'; +import { + ComponentRenderer, + SomeDescriptionModule, +} from 'igniteui-react-core'; +import { SomeDataSource } from './SomeDataSource'; + +// ── Module registration (unchanged from class original) ─────────────────────── +const mods: any[] = [IgrSomeComponentModule, IgrItemLegendModule]; +mods.forEach(m => m.register()); + +// ── Module-level constants (for static data, moved from initData()) ─────────── +const STATIC_DATA = [ + { label: 'A', value: 30 }, + { label: 'B', value: 70 }, +]; + +// ── Module-level pure functions (moved from class instance methods) ─────────── +function createTooltip(series: any, item: any): string { + return `${item.label}: ${item.value}`; +} + +// ── One-time side effects (moved from constructor) ──────────────────────────── +// registerIconFromText('icon-name', SVG_STRING, 'material'); + +export default function Sample() { + + // ── State (replaces this.state = {...} in constructor) ──────────────────── + const [transitionMode, setTransitionMode] = useState('Auto'); + // Use for cross-component wiring via prop: + const [legend, setLegend] = useState(null); + + // ── Refs for components/DOM accessed imperatively ───────────────────────── + const componentRef = useRef(null); + // Use for mutable values that must not trigger re-renders: + const intervalRef = useRef(-1); + const dataRef = useRef([]); + // Unmount guard for async operations: + const mountedRef = useRef(true); + + // ── useMemo for lazy data sources and ComponentRenderer ─────────────────── + const dataSource = useMemo(() => new SomeDataSource(), []); + const renderer = useMemo(() => { + const r = new ComponentRenderer(); + SomeDescriptionModule.register(r.context); + return r; + }, []); + + // ── useEffect: componentDidMount (one-time setup) ───────────────────────── + useEffect(() => { + // async data fetch: + // SomeService.getData().then(result => { if (mountedRef.current) setData(result); }); + + // native event listeners: + // const el = domRef.current; + // el?.addEventListener('igcInput', handleInput); + + // cleanup (componentWillUnmount + listener removal): + return () => { + mountedRef.current = false; + if (intervalRef.current >= 0) { + window.clearInterval(intervalRef.current); + } + // el?.removeEventListener('igcInput', handleInput); + }; + }, []); // [] = run once + + // ── useEffect: cross-component imperative wiring ────────────────────────── + // (omit dependency array so it runs after every render) + useEffect(() => { + if (componentRef.current && legend) { + (componentRef.current as any).legend = legend; + } + }); + + // ── useCallback: event handlers (replaces bound methods) ───────────────── + const onTransitionModeChanged = useCallback( + (e: React.ChangeEvent) => { + setTransitionMode(e.target.value); + }, + [] + ); + + const onReplayClick = useCallback(() => { + componentRef.current?.replayTransitionIn(); + }, []); + + // ── Render (replaces public render(): JSX.Element) ──────────────────────── + return ( +
+ {/* Cross-component wiring via prop (Section 6) */} + { if (r) setLegend(r); }} + orientation="Horizontal" + /> + + {/* Standard ref (Section 5) */} + +
+ ); +} + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render(); +``` + +--- + +## Quick-reference cheat sheet + +| Class | Functional | +|---|---| +| `extends React.Component` | `function Sample() {}` | +| `constructor(props) { super(props); }` | *(deleted)* | +| `this.handler = this.handler.bind(this)` | *(deleted — use `useCallback`)* | +| `this.state = { k: v }` | `const [k, setK] = useState(v)` | +| `this.setState({ k: newV })` | `setK(newV)` | +| `this.setState(prev => ...)` | `setK(prev => ...)` | +| `public field: T` (stored component instance — used as prop) | `const [field, setField] = useState(null)` | +| `public field: T` (stored ref — accessed imperatively) | `const fieldRef = useRef(null)` | +| `public counter: number = 0` (mutable, no render) | `const counterRef = useRef(0)` | +| `get lazy() { if (!_f) _f = new F(); return _f; }` | `const lazy = useMemo(() => new F(), [])` | +| `get renderer() { ... register modules ... }` | `const renderer = useMemo(() => { ...; return r; }, [])` | +| `componentDidMount()` | `useEffect(() => { ... }, [])` | +| `componentWillUnmount()` | `useEffect(() => { return () => { cleanup }; }, [])` | +| `this.handler = (e) => { ... }` (class field arrow) | `const handler = useCallback((e) => { ... }, [deps])` | +| `public method(args) { /* pure */ }` | Module-level function `function method(args) {}` | +| `initData() { this.data = [...] }` | Module-level `const DATA = [...]` | +| `registerIconFromText(...)` in constructor | Module scope before component | +| `this.chart.someMethod()` | `chartRef.current?.someMethod()` | +| `legend={this.legend}` (may be `undefined`) | `legend={legend ?? undefined}` | +| `ref={this.onChartRef}` (bound callback, only assigns field) | `ref={chartRef}` (useRef object — React handles assignment automatically) | +| `ref={this.onChartRef}` (bound callback that also triggers side effects) | callback ref via `useCallback` (see Section 13) | diff --git a/FUNCTIONAL_REFACTORING_README.md b/FUNCTIONAL_REFACTORING_README.md new file mode 100644 index 0000000000..efb2aed82c --- /dev/null +++ b/FUNCTIONAL_REFACTORING_README.md @@ -0,0 +1,251 @@ +# Functional Component Refactoring – Summary + +This document describes the refactoring of **12 class-based React samples** to **functional components** following modern React best practices (React 16.8+). + +Each original `index.tsx` is left unchanged. A new `*Functional.tsx` file has been created alongside it as the functional counterpart. + +> **Generating new samples?** See [`FUNCTIONAL_COMPONENT_GENERATION_GUIDE.md`](./FUNCTIONAL_COMPONENT_GENERATION_GUIDE.md) for the complete construct-by-construct generation reference with class templates, functional templates, and rules for every pattern used across these samples. + +--- + +## Why Functional Components? + +| Concern | Class component | Functional component | +|---|---|---| +| Boilerplate | Requires `constructor`, `this`, `.bind()` | Plain function, no `this` | +| State | `this.state` + `setState` | `useState` hook | +| Lifecycle | `componentDidMount`, `componentDidUpdate`, `componentWillUnmount` | `useEffect` hook (unified) | +| Refs | `React.createRef()` / callback refs stored on `this` | `useRef` hook | +| Expensive computations | Lazy getter or instance field | `useMemo` hook | +| Stable callbacks | Manual `.bind()` in constructor | `useCallback` hook | +| Code sharing | HOCs / render-props (complex) | Custom hooks (simple) | + +--- + +## Sample 1 – Expansion Panel: Properties & Events + +**Files** +- Original: `samples/layouts/expansion-panel/properties-and-events/src/index.tsx` +- Functional: `samples/layouts/expansion-panel/properties-and-events/src/ExpansionPanelPropsAndEventsFunctional.tsx` + +**Feature coverage:** `properties`, `event handlers`, `timed state updates` + +| Class pattern | Functional equivalent | Why | +|---|---|---| +| `this.state = { subtitleClass, eventSpanClass, eventSpanText }` | Three `useState` calls | Fine-grained state is cleaner and avoids stale state merging pitfalls | +| Manual `.bind()` in constructor | Not needed | Arrow functions and `useCallback` capture the right scope naturally | +| `this.setState(...)` inside `onExpansionPanelClosed` / `onExpansionPanelOpened` | State setter calls inside `useCallback` handlers | State setters are stable—no re-binding needed | +| Duplicated open/close logic | Extracted shared `showEvent` helper (called from both handlers) | Reduces duplication, improves readability | +| `window.clearTimeout(undefined)` (no-op) | Removed (was a bug in original) | Calling `clearTimeout(undefined)` has no effect; correct approach omits it | + +--- + +## Sample 2 – Accordion: Customization + +**Files** +- Original: `samples/layouts/accordion/customization/src/index.tsx` +- Functional: `samples/layouts/accordion/customization/src/AccordionCustomizationFunctional.tsx` + +**Feature coverage:** `complex state`, `multiple event handlers`, `refs`, `icon registration` + +| Class pattern | Functional equivalent | Why | +|---|---|---| +| Private class field `categories` + mutable copy in `setState` | `useState` with immutable update (`prev.map(...)`) | Avoids direct mutation; React can track changes correctly | +| `this.dateTimeInput` callback ref | `useRef` | `useRef` is the idiomatic hook for persistent mutable DOM/component references | +| `this.dateTimeInputRef` callback bound in constructor | `ref={dateTimeInputRef}` with `useRef` | Simpler and no binding required | +| `registerIconFromText` called inside `constructor` | Moved to module scope (runs once on import) | Icon registration is a one-time side effect, not tied to component lifecycle | +| `INITIAL_CATEGORIES` as class field | Extracted as a `const` outside the component | Module-level constants are created once, not on every render | +| Mutable `categoriesCopy[i].checked = ...` then `setState` | Immutable `prev.map(c => ...)` inside `setCategories` | Avoids accidentally mutating React state | + +--- + +## Sample 3 – Stepper: Linear (Multi-step Form) + +**Files** +- Original: `samples/layouts/stepper/linear/src/index.tsx` +- Functional: `samples/layouts/stepper/linear/src/LinearStepperFunctional.tsx` + +**Feature coverage:** `refs`, `componentDidMount` (event listeners), `component updates`, `form validation state` + +| Class pattern | Functional equivalent | Why | +|---|---|---| +| `React.createRef()` as class field | `useRef` | Hooks-based ref; same semantics, no `this` | +| `componentDidMount` adding native `igcInput` listeners | `useEffect(() => { ... return cleanup }, [linear, checkActiveStepValidity])` | `useEffect` handles both mount *and* re-attachment when dependencies change, with automatic cleanup via the returned function | +| `activeStepIndex` as class field (mutable between renders) | Step index derived inside `checkActiveStepValidity` at call time | Avoids stale-closure issues; derives from the stepper's current DOM state on each call | +| Manual `.bind(this)` for `onSwitchChange` | `useCallback` | Stable reference without manual binding | +| `this.state.linear` accessed inside `onInput` | `linear` passed directly into `checkActiveStepValidity` | Avoids stale closure—the latest value is always passed explicitly | + +--- + +## Sample 4 – Pivot Grid: Features (Data Operations) + +**Files** +- Original: `samples/grids/pivot-grid/features/src/index.tsx` +- Functional: `samples/grids/pivot-grid/features/src/PivotGridFeaturesFunctional.tsx` + +**Feature coverage:** `data operations`, `complex configuration objects`, `refs`, `aggregate functions` + +| Class pattern | Functional equivalent | Why | +|---|---|---| +| Lazy getter with `_pivotConfiguration1` backing field | `useMemo(() => { ... }, [])` | `useMemo` with an empty dependency array is the hook equivalent of a lazy getter—builds once, memoizes forever | +| `_pivotDataFlat` backing field with lazy getter | `useMemo(() => new PivotDataFlat(), [])` | Same pattern—data source created once and kept stable across renders | +| Aggregate functions as `public` instance methods | Promoted to module-level plain functions | Aggregate logic is stateless; module-level functions are garbage-collected-friendly and don't capture `this` | +| `private gridRef(r)` callback ref + `this.setState({})` to trigger re-render | `useRef` | Standard hook ref; forced re-render on ref assignment is no longer needed | +| `IgrPivotGridModule` registered in module scope | Unchanged—kept at module scope | Already correct; module registration is a one-time side effect | + +--- + +## Sample 5 – Geo Map: Binding Data from CSV (Async Data Loading) + +**Files** +- Original: `samples/maps/geo-map/binding-data-csv/src/index.tsx` +- Functional: `samples/maps/geo-map/binding-data-csv/src/MapBindingDataCSVFunctional.tsx` + +**Feature coverage:** `componentDidMount`, `async data fetching`, `refs`, `tooltip rendering` + +| Class pattern | Functional equivalent | Why | +|---|---|---| +| `public geoMap: IgrGeographicMap` class field | `useRef` | Refs are the correct way to hold mutable, non-render-triggering values in functional components | +| `onMapRef` callback bound in constructor | `ref={geoMapRef}` with `useRef` | Object refs work directly with Ignite UI components; no manual binding | +| `componentDidMount` + `this.onDataLoaded` | `useEffect(() => { fetch(...).then(onDataLoaded) }, [onDataLoaded])` | `useEffect` with an empty/stable dep array runs once after mount, matching `componentDidMount` semantics | +| `this.geoMap` accessed inside `onDataLoaded` | `geoMapRef.current` accessed inside `onDataLoaded` `useCallback` | Reads the latest ref value at call time—no stale reference | +| `createTooltip` as instance method | Promoted to module-level function | Tooltip renderer is stateless and pure; module scope avoids re-creation and `this` | + +--- + +## Sample 6 – Calendar: Disabled Dates (Constructor State Initialization) + +**Files** +- Original: `samples/scheduling/calendar/disabled-dates/src/index.tsx` +- Functional: `samples/scheduling/calendar/disabled-dates/src/CalendarDisabledDatesFunctional.tsx` + +**Feature coverage:** `properties`, `constructor state initialization`, `stable object references` + +| Class pattern | Functional equivalent | Why | +|---|---|---| +| `constructor` computing `disabledDates` and placing it in `this.state` | `useMemo(() => [...], [])` | `useMemo` with an empty dependency array runs once (equivalent to constructor) and memoizes the result, preventing re-creation on every render | +| `this.state.disabledDates` passed as prop | `disabledDates` variable from `useMemo` | Direct variable reference—cleaner and no `this` required | +| Empty `constructor` with `super(props)` | No constructor needed | Functional components have no constructor | + +--- + +## Sample 7 – Tile Manager: Actions (DOM Manipulation + Event Handlers) + +**Files** +- Original: `samples/layouts/tile-manager/actions/src/index.tsx` +- Functional: `samples/layouts/tile-manager/actions/src/TileManagerActionsFunctional.tsx` + +**Feature coverage:** `event handlers`, `DOM manipulation`, `icon registration`, `class field arrow functions` + +| Class pattern | Functional equivalent | Why | +|---|---|---| +| Class field arrow functions (`private onCustomOneClick = (event) => {}`) | `useCallback` | `useCallback` provides stable handler references across renders, preventing unnecessary child re-renders | +| `registerIconFromText` calls in `constructor` | Moved to module scope | Icon registration is a pure side effect; moving it outside the component means it runs exactly once per module load, not once per instance | +| `actionsSlot.parentElement?.querySelectorAll(...)` in original | Simplified to `tile.querySelectorAll('.additional-action')` | The `actionsSlot` traversal was unnecessarily indirect; querying from the tile element is more direct and correct | +| `this.` prefix on all methods and properties | No `this` required | Functions and variables are in lexical scope; arrow functions in `useCallback` close over them naturally | + +--- + +## Sample 8 – Data Pie Chart: Legend (Cross-Ref Wiring + Lazy Getters + ComponentRenderer) + +**Files** +- Original: `samples/charts/data-pie-chart/legend/src/index.tsx` +- Functional: `samples/charts/data-pie-chart/legend/src/DataPieChartLegendFunctional.tsx` + +**Feature coverage:** `cross-component ref wiring`, `lazy data getter`, `ComponentRenderer`, `props as component instances` + +| Class pattern | Functional equivalent | Why | +|---|---|---| +| `private legend: IgrItemLegend` + callback ref `legendRef(r)` calling `setState({})` | `const [legend, setLegend] = useState(null)` | Storing the component instance in state causes a re-render when it is first set, allowing it to be passed as a prop to the chart | +| `private chart: IgrDataPieChart` + callback ref `chartRef(r)` calling `setState({})` | `const [chart, setChart] = useState(null)` (unused beyond triggering re-render) | Same pattern—state update triggers the render that delivers the `legend` prop to the chart | +| Lazy getter `get energyGlobalDemand()` with backing field `_energyGlobalDemand` | `useMemo(() => new EnergyGlobalDemand(), [])` | Created once, stable across renders | +| Lazy getter `get renderer()` with backing field `_componentRenderer` that registers modules | `useMemo(() => { const r = new ComponentRenderer(); ...; return r; }, [])` | ComponentRenderer and its context registrations are created once | +| `legend={this.legend}` in JSX (initially `undefined` until ref fires) | `legend={legend ?? undefined}` | `null ?? undefined` falls back to `undefined`, keeping the prop absent until the legend is ready | + +--- + +## Sample 9 – Doughnut Chart: Legend (Cross-Ref Wiring on Ring Series) + +**Files** +- Original: `samples/charts/doughnut-chart/legend/src/index.tsx` +- Functional: `samples/charts/doughnut-chart/legend/src/DoughnutChartLegendFunctional.tsx` + +**Feature coverage:** `cross-component ref wiring`, `ring series legend prop`, `lazy data getter` + +| Class pattern | Functional equivalent | Why | +|---|---|---| +| `private legend: IgrItemLegend` + callback ref calling `setState({})` | `useState(null)` | Same cross-wiring pattern as the DataPieChart legend sample | +| `private chart: IgrDoughnutChart` + callback ref | `useRef` not needed—the chart does not need to be accessed imperatively | The only wiring needed is passing `legend` as a prop on `IgrRingSeries`; no ref required on the chart itself | +| `` | `` | Prop supplied once the legend state is set | +| Lazy getter `get energyGlobalDemand()` | `useMemo(() => new EnergyGlobalDemand(), [])` | Stable data source reference | + +--- + +## Sample 10 – Financial Chart: Overview (Async Data + Imperative Legend Wiring) + +**Files** +- Original: `samples/charts/financial-chart/overview/src/index.tsx` +- Functional: `samples/charts/financial-chart/overview/src/FinancialChartOverviewFunctional.tsx` + +**Feature coverage:** `async data fetching`, `cross-ref imperative wiring`, `useEffect`, `state initialization` + +| Class pattern | Functional equivalent | Why | +|---|---|---| +| `this.initData()` called in constructor, which calls `this.setState({ data: stocks })` on resolution | `useEffect(() => { StocksHistory.getMultipleStocks().then(stocks => setData(stocks)); }, [])` | `useEffect` with empty deps runs once after mount—equivalent to constructor-time async initiation | +| `public data: any[]` as class field initialized to `[]` | `const [data, setData] = useState([])` | Async result stored in state; initial render uses empty array | +| Callback refs that cross-wire each other: `this.chart.legend = this.legend` / `this.legend` = ... | `useRef` for both + `useEffect(() => { if (chart && legend) chart.legend = legend; })` running after every render | The effectless dependency array means the wiring is applied whenever either ref changes, guaranteeing the assignment happens once both are available | + +--- + +## Sample 11 – Category Chart: High Frequency (setInterval + componentWillUnmount + notifyInsertItem) + +**Files** +- Original: `samples/charts/category-chart/high-frequency/src/index.tsx` +- Functional: `samples/charts/category-chart/high-frequency/src/CategoryChartHighFrequencyFunctional.tsx` + +**Feature coverage:** `setInterval`, `componentWillUnmount`, `imperative chart notification API`, `mixed ref/state pattern` + +| Class pattern | Functional equivalent | Why | +|---|---|---| +| `public interval: number = -1` class field | `const intervalRef = useRef(-1)` | Interval ID must survive re-renders but must never cause them; `useRef` is the correct container | +| `public chart: IgrCategoryChart` class field | `const chartRef = useRef(null)` | Same—chart ref needs to be stable and readable inside the interval callback | +| `public data: any[]` mutable array class field | `const dataRef = useRef(...)` | The data array is mutated in-place for the chart's `notifyInsertItem`/`notifyRemoveItem` API; storing it in a ref avoids triggering re-renders on every tick | +| `public dataPoints: number` / `public dataIndex: number` class fields | `useRef` for each | Values mutated in event handlers without needing re-renders | +| `componentWillUnmount` clearing the interval | Cleanup function returned from `useEffect`: `return () => { clearInterval(intervalRef.current) }` | Collocates setup and teardown; cleanup runs when component unmounts | +| `onChartRef` callback ref calling `this.onChartInit()` | `onChartRef` callback calling `setupInterval()` | `useCallback`-wrapped ref callback starts the interval once the chart is available | +| `this.state.dataFeedAction === "Stop"` read inside `tick()` | Accessing `setState` updater's `prev` arg inside tick | The tick is inside a `setInterval` closure; reading the latest state safely requires using the functional update form of `setState` | + +--- + +## Sample 12 – Category Chart: Line Chart with Animations (State + Event Handlers + Replay) + +**Files** +- Original: `samples/charts/category-chart/line-chart-with-animations/src/index.tsx` +- Functional: `samples/charts/category-chart/line-chart-with-animations/src/CategoryChartLineChartWithAnimationsFunctional.tsx` + +**Feature coverage:** `state`, `event handlers`, `chart ref for imperative API`, `module-level constants` + +| Class pattern | Functional equivalent | Why | +|---|---|---| +| `public data: any[]` field assigned in `initData()` then used as prop | Module-level `CHART_DATA` constant | Data is static; no side effects needed; module constant is cleaner and avoids recreation | +| `this.initData()` called inside `onTransitionInModeChanged` (to reset chart) | Removed—`CHART_DATA` is already stable | The original called `initData()` on mode change to "re-trigger" the chart; with a stable reference the chart already re-renders on `transitionInMode` state change | +| `this.state.transitionLabel`, `transitionInDuration`, `transitionInMode` | Three `useState` calls | Fine-grained state; each setter is independent | +| `public chart: IgrCategoryChart` field + `this.onChartRef` callback | `const chartRef = useRef` + `const onChartRef = useCallback` | `useRef` for the chart instance; `useCallback` for a stable ref callback | +| `this.chart.replayTransitionIn()` inside `onReloadChartClick` | `chartRef.current?.replayTransitionIn()` | Optional chaining guards against the chart not yet being set | + +--- + +## General Patterns Applied Across All Samples + +| Class component pattern | Functional equivalent | Notes | +|---|---|---| +| `extends React.Component` | `function ComponentName() {}` | No inheritance needed | +| `constructor(props) { super(props); this.state = {...} }` | `const [x, setX] = useState(...)` per piece of state | Each state slice is independent; no need to spread/merge the full state object | +| `this.setState({ key: value })` | `setKey(value)` | Direct setter; React batches updates automatically in React 18 | +| `public render(): JSX.Element { return (...) }` | Component body `return (...)` | The function body *is* the render method | +| Manual `.bind(this)` in constructor | Not needed (hooks + arrow functions) | Lexical `this` in functional components is never needed | +| `componentDidMount()` | `useEffect(() => { ... }, [])` | Empty dependency array = runs once after first render | +| `componentWillUnmount()` | Cleanup function returned from `useEffect` | Collocates setup and teardown logic | +| Lazy instance getter (backing field + null check) | `useMemo(() => ..., [])` | Memoizes expensive computations; empty deps = computed once | +| Callback ref stored on `this` | `useRef` + `ref={refObj}` | Ref object persists for the component's lifetime | +| `ReactDOM.createRoot(...).render()` | `ReactDOM.createRoot(...).render()` | Unchanged—only the component name changes | diff --git a/browser/tasks/Transformer.ts b/browser/tasks/Transformer.ts index e037fe3109..1f56dc8ced 100644 --- a/browser/tasks/Transformer.ts +++ b/browser/tasks/Transformer.ts @@ -164,6 +164,8 @@ class Transformer { public static process(samples: SampleInfo[]): void { + let functionalExtras: SampleInfo[] = []; + for (const info of samples) { // console.log("Transformer processing: " + info.SampleFolderPath); @@ -192,13 +194,18 @@ class Transformer { // console.log("Transformer fileNames ..."); let fileNames = []; + let functionalFileNames = []; let fileFound = []; for (const filePath of info.SampleFilePaths) { // console.log(filePath); fileFound.push(filePath); if (Strings.includes(filePath, igConfig.SampleFileExtension) && Strings.excludes(filePath, igConfig.SampleFileExclusions, true)) { - fileNames.push(filePath); + if (filePath.endsWith('Functional.tsx')) { + functionalFileNames.push(filePath); + } else { + fileNames.push(filePath); + } } } @@ -212,72 +219,100 @@ class Transformer { } else if (fileNames.length > 1) { console.log("WARNING Transformer cannot decide which " + igConfig.SampleFileExtension + " file to use for sample name: "); console.log(" - " + fileNames.join("\n - ")); - } else { // only one .tsx file per sample + } else { // only one .tsx file per sample (excluding Functional variants) // console.log("Transformer fileNames[0]= " + fileNames[0]); - info.SampleFilePath = fileNames[0]; - info.SampleFileName = this.getFileName(info.SampleFilePath); - info.SampleFileSourcePath = "./src/" + info.SampleFileName; - info.SampleFileSourceCode = transFS.readFileSync(info.SampleFilePath, "utf8"); - - // let classExp = new RegExp(/(export.default.class.)(.*)(.\{)/g); - // let className = info.SampleFileSourceCode.match(classExp); - // let className = info.SampleFileSourceCode.match(classExp)[0]; - // console.log("Transformer className1= " + className); - // className = className.replace(/(export.default.class.)(.*)(.extends.*)/g, '$2'); - // className = className.trim(); - // console.log("Transformer className= " + className); - // using folder names to make sure each sample has unique class name - let className = info.ComponentFolder + "-" + info.SampleFolderName; - className = Strings.replace(className, "/", " "); - className = Strings.replace(className, "-", " "); - className = Strings.toTitleCase(className); - className = Strings.replace(className, " ", ""); - info.SampleFileSourceClass = className; - - // console.log("Transformer SampleFileSourceClass=" + info.SampleFileSourceClass); - - let sampleBlocks = this.getSampleBlocks(info.SampleFileSourceCode); - info.SampleImportLines = sampleBlocks.ImportLines; - info.SampleImportFiles = sampleBlocks.ImportFiles; - info.SampleImportPackages = sampleBlocks.ImportPackages; - - // info.SampleImportName = info.SampleFileName.replace('.tsx','').replace('.ts',''); - // info.SampleImportPath = './' + info.ComponentFolder + '/' + info.SampleFolderName + '/' + info.SampleImportName; - // info.SampleImportName = info.SampleFileSourceClass.replace('.ts', ''); - // using folder names to make sure each sample has unique class name - info.SampleImportName = info.ComponentFolder + "-" + info.SampleFolderName; - info.SampleImportName = Strings.replace(info.SampleImportName, "/", " "); - info.SampleImportName = Strings.replace(info.SampleImportName, "-", " "); - info.SampleImportName = Strings.toTitleCase(info.SampleImportName); - info.SampleImportName = Strings.replace(info.SampleImportName, " ", ""); - - // info.SampleImportPath = './' + info.ComponentFolder + '/' + info.SampleFolderName + '/' + info.SampleImportName; - info.SampleImportPath = './' + info.ComponentFolder + '/' + info.SampleFolderName + '/index'; - - // console.log("Transformer SampleDisplayName ..."); - info.SampleDisplayName = Strings.splitCamel(info.SampleFileSourceClass); - info.SampleDisplayName = Strings.replace(info.SampleDisplayName, info.ComponentName, ""); - info.SampleDisplayName = Strings.replace(info.SampleDisplayName, igConfig.SampleFileExtension, ""); - info.SampleDisplayName = Strings.replace(info.SampleDisplayName, "Map Type ", ""); - info.SampleDisplayName = Strings.replace(info.SampleDisplayName, "Map Binding ", "Binding "); - info.SampleDisplayName = Strings.replace(info.SampleDisplayName, "Map Display ", "Display "); - info.SampleDisplayName = Strings.replace(info.SampleDisplayName, "Data Chart Type ", ""); - info.SampleDisplayName = Strings.replace(info.SampleDisplayName, info.ComponentName + " ", ""); - info.SampleDisplayName = info.SampleDisplayName.trim(); - - // console.log("Transformer Sandbox ..."); - info.SandboxUrlView = this.getSandboxUrl(info, igConfig.SandboxUrlView); - info.SandboxUrlEdit = this.getSandboxUrl(info, igConfig.SandboxUrlEdit); - info.SandboxUrlShort = this.getSandboxUrl(info, igConfig.SandboxUrlShort); - - // console.log("Transformer getDocsUrl ..."); - info.DocsUrl = this.getDocsUrl(info); - // console.log("SAMPLE " + info.SampleFilePath + " => " + info.SampleDisplayName); + this.processSampleFile(info, fileNames[0]); + + // create additional SampleInfo entries for each Functional variant + for (const funcPath of functionalFileNames) { + let funcInfo = new SampleInfo(); + funcInfo.ComponentGroup = info.ComponentGroup; + funcInfo.ComponentFolder = info.ComponentFolder; + funcInfo.ComponentName = info.ComponentName; + funcInfo.SampleFolderPath = info.SampleFolderPath; + funcInfo.SampleFolderName = info.SampleFolderName; + funcInfo.SampleFilePaths = info.SampleFilePaths; + funcInfo.SampleFileNames = info.SampleFileNames; + + let funcFileName = this.getFileName(funcPath); + let funcImportName = funcFileName.replace('.tsx', ''); + + funcInfo.SampleFilePath = funcPath; + funcInfo.SampleFileName = funcFileName; + funcInfo.SampleFileSourcePath = "./src/" + funcFileName; + funcInfo.SampleFileSourceCode = transFS.readFileSync(funcPath, "utf8"); + funcInfo.SampleFileSourceClass = funcImportName; + + funcInfo.SampleImportName = funcImportName; + funcInfo.SampleImportPath = './' + info.ComponentFolder + '/' + info.SampleFolderName + '/' + funcImportName; + + funcInfo.SampleRouteNew = info.SampleRouteNew + '-functional'; + funcInfo.SampleRouteOld = info.SampleRouteOld + '-functional'; + + funcInfo.SampleDisplayName = info.SampleDisplayName + ' (Functional)'; + + funcInfo.SandboxUrlView = info.SandboxUrlView; + funcInfo.SandboxUrlEdit = info.SandboxUrlEdit; + funcInfo.SandboxUrlShort = info.SandboxUrlShort; + funcInfo.DocsUrl = info.DocsUrl; + + functionalExtras.push(funcInfo); + } } // console.log(info.SampleFolderPath + " => " + info.SampleRouteNew + " => " + info.SampleDisplayName); } + + // append functional variants so they appear in routing right after their class counterparts + for (const funcInfo of functionalExtras) { + samples.push(funcInfo); + } + } + + private static processSampleFile(info: SampleInfo, filePath: string): void { + info.SampleFilePath = filePath; + info.SampleFileName = this.getFileName(info.SampleFilePath); + info.SampleFileSourcePath = "./src/" + info.SampleFileName; + info.SampleFileSourceCode = transFS.readFileSync(info.SampleFilePath, "utf8"); + + // using folder names to make sure each sample has unique class name + let className = info.ComponentFolder + "-" + info.SampleFolderName; + className = Strings.replace(className, "/", " "); + className = Strings.replace(className, "-", " "); + className = Strings.toTitleCase(className); + className = Strings.replace(className, " ", ""); + info.SampleFileSourceClass = className; + + let sampleBlocks = this.getSampleBlocks(info.SampleFileSourceCode); + info.SampleImportLines = sampleBlocks.ImportLines; + info.SampleImportFiles = sampleBlocks.ImportFiles; + info.SampleImportPackages = sampleBlocks.ImportPackages; + + // using folder names to make sure each sample has unique class name + info.SampleImportName = info.ComponentFolder + "-" + info.SampleFolderName; + info.SampleImportName = Strings.replace(info.SampleImportName, "/", " "); + info.SampleImportName = Strings.replace(info.SampleImportName, "-", " "); + info.SampleImportName = Strings.toTitleCase(info.SampleImportName); + info.SampleImportName = Strings.replace(info.SampleImportName, " ", ""); + + info.SampleImportPath = './' + info.ComponentFolder + '/' + info.SampleFolderName + '/index'; + + info.SampleDisplayName = Strings.splitCamel(info.SampleFileSourceClass); + info.SampleDisplayName = Strings.replace(info.SampleDisplayName, info.ComponentName, ""); + info.SampleDisplayName = Strings.replace(info.SampleDisplayName, igConfig.SampleFileExtension, ""); + info.SampleDisplayName = Strings.replace(info.SampleDisplayName, "Map Type ", ""); + info.SampleDisplayName = Strings.replace(info.SampleDisplayName, "Map Binding ", "Binding "); + info.SampleDisplayName = Strings.replace(info.SampleDisplayName, "Map Display ", "Display "); + info.SampleDisplayName = Strings.replace(info.SampleDisplayName, "Data Chart Type ", ""); + info.SampleDisplayName = Strings.replace(info.SampleDisplayName, info.ComponentName + " ", ""); + info.SampleDisplayName = info.SampleDisplayName.trim(); + + info.SandboxUrlView = this.getSandboxUrl(info, igConfig.SandboxUrlView); + info.SandboxUrlEdit = this.getSandboxUrl(info, igConfig.SandboxUrlEdit); + info.SandboxUrlShort = this.getSandboxUrl(info, igConfig.SandboxUrlShort); + + info.DocsUrl = this.getDocsUrl(info); } public static getSandboxUrl(sampleInfo: SampleInfo, sandboxUrlFormat: string): string { diff --git a/index.functional.template.tsx b/index.functional.template.tsx new file mode 100644 index 0000000000..1feb09c56f --- /dev/null +++ b/index.functional.template.tsx @@ -0,0 +1,88 @@ +import React, { + //insert hooksImports + //end hooksImports +} from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; + +//insert vmImports +//end vmImports +//insert modulesImports +//end modulesImports +//insert bindingImports +//end bindingImports +//insert descriptionImports +//end descriptionImports +//insert vmLibraryImports +//end vmLibraryImports +//insert handlersImports +//end handlersImports +//insert templateImports +//end templateImports + +//ifdef webgrids +import 'igniteui-react-grids/grids/themes/light/bootstrap.css'; +//endifdef webgrids +//ifdef editor +import 'igniteui-webcomponents/themes/light/bootstrap.css'; +//endifdef editor + +//ifdef modulesRegister +const mods: any[] = [ + //insert modulesRegister + //end modulesRegister +]; +mods.forEach((m) => m.register()); +//endifdef modulesRegister + +//ifdef moduleRegistrations +//insert moduleRegistrations +//end moduleRegistrations +//endifdef moduleRegistrations + +//ifdef moduleConstants +//insert moduleConstants +//end moduleConstants +//endifdef moduleConstants + +//ifdef moduleFunctions +//insert moduleFunctions +//end moduleFunctions +//endifdef moduleFunctions + +export default function Sample() { + + //ifdef useState + //insert useState + //end useState + //endifdef useState + + //ifdef useRef + //insert useRef + //end useRef + //endifdef useRef + + //ifdef useMemo + //insert useMemo + //end useMemo + //endifdef useMemo + + //ifdef useEffect + //insert useEffect + //end useEffect + //endifdef useEffect + + //ifdef useCallback + //insert useCallback + //end useCallback + //endifdef useCallback + + return ( + //insert render + //end render + ); +} + +// rendering above component to the React DOM +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render(); diff --git a/samples/charts/category-chart/high-frequency/src/CategoryChartHighFrequencyFunctional.tsx b/samples/charts/category-chart/high-frequency/src/CategoryChartHighFrequencyFunctional.tsx new file mode 100644 index 0000000000..8f44db1c42 --- /dev/null +++ b/samples/charts/category-chart/high-frequency/src/CategoryChartHighFrequencyFunctional.tsx @@ -0,0 +1,169 @@ +import React, { useState, useRef, useEffect, useCallback } from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; +import { IgrCategoryChart } from 'igniteui-react-charts'; +import { IgrCategoryChartModule } from 'igniteui-react-charts'; +import { CategoryChartSharedData } from './CategoryChartSharedData'; + +IgrCategoryChartModule.register(); + +export default function CategoryChartHighFrequency() { + const INITIAL_DATA_POINTS = 500; + + // Mutable values that must not trigger re-renders are kept in refs + const chartRef = useRef(null); + const dataRef = useRef(CategoryChartSharedData.generateItems(100, INITIAL_DATA_POINTS, false)); + const dataIndexRef = useRef(dataRef.current.length); + const dataPointsRef = useRef(INITIAL_DATA_POINTS); + const refreshMsRef = useRef(5); + const intervalRef = useRef(-1); + + const [state, setState] = useState({ + dataFeedAction: 'Start', + dataInfo: CategoryChartSharedData.toShortString(INITIAL_DATA_POINTS), + dataPoints: INITIAL_DATA_POINTS, + dataSource: dataRef.current, + refreshInterval: 5, + refreshInfo: '5ms', + }); + + const mountedRef = useRef(true); + + const setupInterval = useCallback(() => { + if (intervalRef.current >= 0) { + window.clearInterval(intervalRef.current); + intervalRef.current = -1; + } + intervalRef.current = window.setInterval(() => { + if (!mountedRef.current) return; + setState(prev => { + if (prev.dataFeedAction !== 'Stop') return prev; + + const chart = chartRef.current; + if (!chart) return prev; + + dataIndexRef.current++; + const oldItem = dataRef.current[0]; + const newItem = CategoryChartSharedData.getNewItem(dataRef.current, dataIndexRef.current); + + dataRef.current.push(newItem); + chart.notifyInsertItem(dataRef.current, dataRef.current.length - 1, newItem); + dataRef.current.shift(); + chart.notifyRemoveItem(dataRef.current, 0, oldItem); + + return prev; // no state change – data mutated imperatively on the chart + }); + }, refreshMsRef.current); + }, []); + + // Start interval after chart mounts; clear on unmount (replaces componentWillUnmount) + useEffect(() => { + if (chartRef.current) { + setupInterval(); + } + return () => { + mountedRef.current = false; + if (intervalRef.current >= 0) { + window.clearInterval(intervalRef.current); + intervalRef.current = -1; + } + }; + }, [setupInterval]); + + const onChartRef = useCallback((chart: IgrCategoryChart) => { + if (!chart) return; + chartRef.current = chart; + setupInterval(); + }, [setupInterval]); + + const onDataGenerateClick = useCallback(() => { + dataRef.current = CategoryChartSharedData.generateItems(100, dataPointsRef.current, false); + dataIndexRef.current = dataRef.current.length; + setState(prev => ({ ...prev, dataSource: dataRef.current })); + }, []); + + const onDataFeedClick = useCallback(() => { + setState(prev => ({ + ...prev, + dataFeedAction: prev.dataFeedAction === 'Start' ? 'Stop' : 'Start', + })); + }, []); + + const onDataPointsChanged = useCallback((e: React.ChangeEvent) => { + let num = parseInt(e.target.value, 10); + if (isNaN(num)) num = 10000; + if (num < 100) num = 100; + if (num > 2000) num = 2000; + dataPointsRef.current = num; + setState(prev => ({ + ...prev, + dataPoints: num, + dataInfo: CategoryChartSharedData.toShortString(num), + })); + }, []); + + const onRefreshFrequencyChanged = useCallback((e: React.ChangeEvent) => { + let num = parseInt(e.target.value, 10); + if (isNaN(num)) num = 10; + if (num < 10) num = 10; + if (num > 500) num = 500; + refreshMsRef.current = num; + setState(prev => ({ + ...prev, + refreshInterval: num, + refreshInfo: num + 'ms', + })); + setupInterval(); + }, [setupInterval]); + + return ( +
+
+ + + + + + + + +
+
+ +
+
+ ); +} + +// rendering above component to the React DOM +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render(); diff --git a/samples/charts/category-chart/line-chart-with-animations/src/CategoryChartLineChartWithAnimationsFunctional.tsx b/samples/charts/category-chart/line-chart-with-animations/src/CategoryChartLineChartWithAnimationsFunctional.tsx new file mode 100644 index 0000000000..b5ad9a39a2 --- /dev/null +++ b/samples/charts/category-chart/line-chart-with-animations/src/CategoryChartLineChartWithAnimationsFunctional.tsx @@ -0,0 +1,106 @@ +import React, { useState, useRef, useCallback } from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; +import { IgrCategoryChart } from 'igniteui-react-charts'; +import { IgrCategoryChartModule } from 'igniteui-react-charts'; + +IgrCategoryChartModule.register(); + +const CHART_DATA = [ + { Year: '2009', Europe: 31, China: 21, USA: 19 }, + { Year: '2010', Europe: 43, China: 26, USA: 24 }, + { Year: '2011', Europe: 66, China: 29, USA: 28 }, + { Year: '2012', Europe: 69, China: 32, USA: 26 }, + { Year: '2013', Europe: 58, China: 47, USA: 38 }, + { Year: '2014', Europe: 40, China: 46, USA: 31 }, + { Year: '2015', Europe: 78, China: 50, USA: 19 }, + { Year: '2016', Europe: 13, China: 90, USA: 52 }, + { Year: '2017', Europe: 78, China: 132, USA: 50 }, + { Year: '2018', Europe: 40, China: 134, USA: 34 }, + { Year: '2019', Europe: 80, China: 96, USA: 38 }, +]; + +export default function CategoryChartLineChartWithAnimations() { + const chartRef = useRef(null); + + const [transitionLabel, setTransitionLabel] = useState('1000ms'); + const [transitionInDuration, setTransitionInDuration] = useState(1000); + const [transitionInMode, setTransitionInMode] = useState('Auto'); + + const onTransitionInModeChanged = useCallback((e: React.ChangeEvent) => { + setTransitionInMode(e.target.value); + }, []); + + const onTransitionInDurationChanged = useCallback((e: React.ChangeEvent) => { + const val = parseInt(e.target.value, 10); + setTransitionInDuration(val); + setTransitionLabel(val + 'ms'); + }, []); + + const onReloadChartClick = useCallback(() => { + chartRef.current?.replayTransitionIn(); + }, []); + + return ( +
+
+ Transition Type + + + + +
+ + +
+ ); +} + +// rendering above component to the React DOM +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render(); diff --git a/samples/charts/data-pie-chart/legend/src/DataPieChartLegendFunctional.tsx b/samples/charts/data-pie-chart/legend/src/DataPieChartLegendFunctional.tsx new file mode 100644 index 0000000000..5e467a6043 --- /dev/null +++ b/samples/charts/data-pie-chart/legend/src/DataPieChartLegendFunctional.tsx @@ -0,0 +1,52 @@ +import React, { useState, useMemo } from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; + +import { IgrPropertyEditorPanelModule } from 'igniteui-react-layouts'; +import { IgrDataPieChartModule, IgrItemLegendModule } from 'igniteui-react-charts'; +import { IgrItemLegend, IgrDataPieChart } from 'igniteui-react-charts'; +import { EnergyGlobalDemand } from './EnergyGlobalDemand'; + +const mods: any[] = [ + IgrPropertyEditorPanelModule, + IgrDataPieChartModule, + IgrItemLegendModule +]; +mods.forEach(m => m.register()); + +export default function Sample() { + // useState stores the component instances so that when one becomes available + // React re-renders and the other component can receive it as a prop. + const [legend, setLegend] = useState(null); + const [chart, setChart] = useState(null); + + // useMemo creates these once – equivalent to the lazy backing-field getters in the class + const energyGlobalDemand = useMemo(() => new EnergyGlobalDemand(), []); + + return ( +
+
+ Global Electricity Demand by Energy Use +
+ +
+ { if (r) setLegend(r); }} + orientation="Horizontal" + /> +
+ +
+ { if (r) setChart(r); }} + dataSource={energyGlobalDemand} + legend={legend ?? undefined} + /> +
+
+ ); +} + +// rendering above component in the React DOM +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render(); diff --git a/samples/charts/doughnut-chart/legend/src/DoughnutChartLegendFunctional.tsx b/samples/charts/doughnut-chart/legend/src/DoughnutChartLegendFunctional.tsx new file mode 100644 index 0000000000..b60368bb64 --- /dev/null +++ b/samples/charts/doughnut-chart/legend/src/DoughnutChartLegendFunctional.tsx @@ -0,0 +1,58 @@ +import React, { useState, useMemo } from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; + +import { IgrItemLegendModule, IgrDoughnutChartModule } from 'igniteui-react-charts'; +import { IgrItemLegend, IgrDoughnutChart, IgrRingSeries } from 'igniteui-react-charts'; +import { EnergyGlobalDemand } from './EnergyGlobalDemand'; + +const mods: any[] = [ + IgrItemLegendModule, + IgrDoughnutChartModule +]; +mods.forEach(m => m.register()); + +export default function Sample() { + // useState for legend instance so that when it becomes available + // the component re-renders and can wire the legend prop onto the ring series. + const [legend, setLegend] = useState(null); + + const energyGlobalDemand = useMemo(() => new EnergyGlobalDemand(), []); + + return ( +
+
+ Global Electricity Demand by Energy Use +
+ +
+ { if (r) setLegend(r); }} + orientation="Horizontal" + /> +
+ +
+ + + +
+
+ ); +} + +// rendering above component in the React DOM +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render(); diff --git a/samples/charts/financial-chart/overview/src/FinancialChartOverviewFunctional.tsx b/samples/charts/financial-chart/overview/src/FinancialChartOverviewFunctional.tsx new file mode 100644 index 0000000000..90d121db96 --- /dev/null +++ b/samples/charts/financial-chart/overview/src/FinancialChartOverviewFunctional.tsx @@ -0,0 +1,65 @@ +import React, { useState, useRef, useEffect, useCallback } from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; +import { IgrFinancialChart } from 'igniteui-react-charts'; +import { IgrFinancialChartModule } from 'igniteui-react-charts'; +import { IgrLegend } from 'igniteui-react-charts'; +import { IgrLegendModule } from 'igniteui-react-charts'; +import StocksHistory from './StocksHistory'; + +IgrFinancialChartModule.register(); +IgrLegendModule.register(); + +export default function FinancialChartOverview() { + const [data, setData] = useState([]); + + // Refs hold the component instances; useEffect wires them together once both are set + const chartRef = useRef(null); + const legendRef = useRef(null); + + // Async data fetch replaces initData() called in constructor + setState callback + useEffect(() => { + StocksHistory.getMultipleStocks().then((stocks: any[]) => { + setData(stocks); + }); + }, []); + + // Wire legend → chart imperatively when both refs are ready + useEffect(() => { + if (chartRef.current && legendRef.current) { + chartRef.current.legend = legendRef.current; + } + }); + + return ( +
+
+ Google vs Microsoft Stock Prices +
+ +
+
+
+ +
+
+ ); +} + +// rendering above component to the React DOM +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render(); diff --git a/samples/grids/pivot-grid/features/src/PivotGridFeaturesFunctional.tsx b/samples/grids/pivot-grid/features/src/PivotGridFeaturesFunctional.tsx new file mode 100644 index 0000000000..2a7fac9b0b --- /dev/null +++ b/samples/grids/pivot-grid/features/src/PivotGridFeaturesFunctional.tsx @@ -0,0 +1,133 @@ +import React, { useRef, useMemo } from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; + +import { IgrPivotGridModule } from 'igniteui-react-grids'; +import { + IgrPivotGrid, + IgrPivotConfiguration, + IgrPivotDateDimension, + IgrPivotDimension, + IgrPivotDateDimensionOptions, + SortingDirection, + IgrPivotValue, + IgrPivotAggregator, +} from 'igniteui-react-grids'; +import { PivotDataFlat } from './PivotDataFlat'; + +import 'igniteui-react-grids/grids/themes/light/bootstrap.css'; + +const mods: any[] = [IgrPivotGridModule]; +mods.forEach(m => m.register()); + +function pivotDataFlatAggregateSumSale(_members: any[], data: any[]): any { + return data.reduce((acc, v) => acc + v.ProductUnitPrice * v.NumberOfUnits, 0); +} + +function pivotDataFlatAggregateMinSale(_members: any[], data: any[]): any { + if (data.length === 0) return 0; + const mapped = data.map(x => x.ProductUnitPrice * x.NumberOfUnits); + return mapped.reduce((a, b) => Math.min(a, b)); +} + +function pivotDataFlatAggregateMaxSale(_members: any[], data: any[]): any { + if (data.length === 0) return 0; + const mapped = data.map(x => x.ProductUnitPrice * x.NumberOfUnits); + return mapped.reduce((a, b) => Math.max(a, b)); +} + +export default function Sample() { + const gridRef = useRef(null); + + // useMemo ensures the configuration object is built only once (stable reference) + const pivotConfiguration = useMemo(() => { + const config = {} as IgrPivotConfiguration; + + const dateDimension = new IgrPivotDateDimension(); + dateDimension.memberName = 'Date'; + dateDimension.enabled = true; + + const baseDimension = {} as IgrPivotDimension; + baseDimension.memberName = 'Date'; + baseDimension.enabled = true; + dateDimension.baseDimension = baseDimension; + + const dateOptions = {} as IgrPivotDateDimensionOptions; + dateOptions.years = true; + dateOptions.months = false; + dateOptions.quarters = true; + dateOptions.fullDate = false; + dateDimension.options = dateOptions; + + config.columns = [dateDimension]; + + const productDim = {} as IgrPivotDimension; + productDim.memberName = 'ProductName'; + productDim.sortDirection = SortingDirection.Asc; + productDim.enabled = true; + + const cityDim = {} as IgrPivotDimension; + cityDim.memberName = 'SellerCity'; + cityDim.enabled = true; + + config.rows = [productDim, cityDim]; + + const sellerDim = {} as IgrPivotDimension; + sellerDim.memberName = 'SellerName'; + sellerDim.enabled = true; + + config.filters = [sellerDim]; + + const pivotValue = {} as IgrPivotValue; + pivotValue.member = 'AmountofSale'; + pivotValue.displayName = 'Amount of Sale'; + pivotValue.enabled = true; + + const sumAgg = {} as IgrPivotAggregator; + sumAgg.key = 'SUM'; + sumAgg.label = 'Sum of Sale'; + sumAgg.aggregator = pivotDataFlatAggregateSumSale; + pivotValue.aggregate = sumAgg; + + const sumAgg2 = {} as IgrPivotAggregator; + sumAgg2.key = 'SUM'; + sumAgg2.label = 'Sum of Sale'; + sumAgg2.aggregator = pivotDataFlatAggregateSumSale; + + const minAgg = {} as IgrPivotAggregator; + minAgg.key = 'MIN'; + minAgg.label = 'Minimum of Sale'; + minAgg.aggregator = pivotDataFlatAggregateMinSale; + + const maxAgg = {} as IgrPivotAggregator; + maxAgg.key = 'MAX'; + maxAgg.label = 'Maximum of Sale'; + maxAgg.aggregator = pivotDataFlatAggregateMaxSale; + + pivotValue.aggregateList = [sumAgg2, minAgg, maxAgg]; + config.values = [pivotValue]; + + return config; + }, []); + + // useMemo keeps data source instance stable across re-renders + const pivotData = useMemo(() => new PivotDataFlat(), []); + + return ( +
+
+ +
+
+ ); +} + +// rendering above component to the React DOM +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render(); diff --git a/samples/layouts/accordion/customization/src/AccordionCustomizationFunctional.tsx b/samples/layouts/accordion/customization/src/AccordionCustomizationFunctional.tsx new file mode 100644 index 0000000000..6e432d8de6 --- /dev/null +++ b/samples/layouts/accordion/customization/src/AccordionCustomizationFunctional.tsx @@ -0,0 +1,172 @@ +import React, { useState, useRef, useCallback } from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; +import './AccordionCustomization.css'; +import { + IgrAccordion, + IgrCheckbox, + IgrCheckboxChangeEventArgs, + IgrDateTimeInput, + IgrExpansionPanel, + IgrIcon, + IgrRadio, + IgrRadioGroup, + IgrRating, + IgrRangeSlider, + IgrRadioChangeEventArgs, + IgrRangeSliderValueEventArgs, + IgrComponentDateValueChangedEventArgs, + registerIconFromText, +} from 'igniteui-react'; +import 'igniteui-webcomponents/themes/light/bootstrap.css'; + +type Category = { checked: boolean; type: string }; + +const clearIcon = + ""; +const clockIcon = + ""; + +registerIconFromText('clear', clearIcon, 'material'); +registerIconFromText('clock', clockIcon, 'material'); + +const INITIAL_CATEGORIES: Category[] = [ + { checked: false, type: 'Bike' }, + { checked: false, type: 'Motorcycle' }, + { checked: false, type: 'Car' }, + { checked: false, type: 'Taxi' }, + { checked: false, type: 'Public Transport' }, +]; + +export default function AccordionCustomization() { + const [categories, setCategories] = useState(INITIAL_CATEGORIES); + const [cost, setCost] = useState({ lower: 200, upper: 800 }); + const [rating, setRating] = useState(''); + const [time, setTime] = useState('Time'); + const dateTimeInputRef = useRef(null); + + const categoriesChange = useCallback((e: IgrCheckboxChangeEventArgs, type: string) => { + setCategories(prev => + prev.map(c => c.type === type ? { ...c, checked: e.detail.checked } : c) + ); + }, []); + + const costRangeChange = useCallback((e: IgrRangeSliderValueEventArgs) => { + setCost({ lower: e.detail.lower, upper: e.detail.upper }); + }, []); + + const ratingChange = useCallback((e: IgrRadioChangeEventArgs) => { + if (!e.detail.value) return; + const stars = +e.detail.value; + setRating(`${stars} star${stars > 1 ? 's' : ''} or more`); + }, []); + + const timeChange = useCallback((e: IgrComponentDateValueChangedEventArgs) => { + const s = e.target as IgrDateTimeInput; + const result = + s.value !== null + ? `Arrive before ${e.detail.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}` + : 'Time'; + setTime(result); + }, []); + + const clearTime = useCallback(() => { + dateTimeInputRef.current?.clear(); + setTime('Time'); + }, []); + + return ( +
+
+ + +

+ Categories + {categories.some((c: Category) => c.checked) && ': '} + {categories + .filter((c: Category) => c.checked) + .map((c: Category) => c.type) + .join(', ')} +

+ +
+ {categories.map((c: Category) => ( + categoriesChange(e, c.type)} + > + {c.type} + + ))} +
+
+
+ +

+ Cost: ${cost.lower} to $ + {cost.upper} +

+ + + +
+ +

+ Rating{rating && ': '} + {rating} +

+ + + {[1, 2, 3, 4].map(r => ( + + 1 ? 's' : ''} or more`} + max={5} + value={r + 0.5} + className="size-small" + readOnly={true} + /> + + ))} + + +
+ +

{time}

+ + + + + + + + + + +
+
+
+
+ ); +} + +// rendering above component to the React DOM +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render(); diff --git a/samples/layouts/expansion-panel/properties-and-events/src/ExpansionPanelPropsAndEventsFunctional.tsx b/samples/layouts/expansion-panel/properties-and-events/src/ExpansionPanelPropsAndEventsFunctional.tsx new file mode 100644 index 0000000000..ba21f26881 --- /dev/null +++ b/samples/layouts/expansion-panel/properties-and-events/src/ExpansionPanelPropsAndEventsFunctional.tsx @@ -0,0 +1,49 @@ +import React, { useState, useCallback } from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; +import './ExpansionPanelPropsAndEvents.css'; +import { IgrExpansionPanel } from 'igniteui-react'; +import 'igniteui-webcomponents/themes/light/bootstrap.css'; + +export default function ExpansionPanelPropertiesAndEvents() { + const [subtitleClass, setSubtitleClass] = useState(''); + const [eventSpanClass, setEventSpanClass] = useState('eventSpanHidden'); + const [eventSpanText, setEventSpanText] = useState('none'); + + const showEvent = useCallback((text: string, newSubtitleClass: string) => { + setSubtitleClass(newSubtitleClass); + setEventSpanClass('eventSpanShown'); + setEventSpanText(text); + + window.setTimeout(() => { + setEventSpanClass('eventSpanHidden'); + }, 2000); + }, []); + + const onExpansionPanelClosed = useCallback(() => { + showEvent('Closed event fired!', ''); + }, [showEvent]); + + const onExpansionPanelOpened = useCallback(() => { + showEvent('Opened event fired!', 'subtitleHidden'); + }, [showEvent]); + + return ( +
+ +

Golden Retriever

+

Medium-large gun dog

+ The Golden Retriever is a medium-large gun dog that retrieves shot waterfowl, such as ducks + and upland game birds, during hunting and shooting parties.[3] The name retriever refers to the breeds ability + to retrieve shot game undamaged due to their soft mouth. Golden retrievers have an instinctive love of water, and + are easy to train to basic or advanced obedience standards. +
+ + {eventSpanText} +
+ ); +} + +// rendering above component to the React DOM +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render(); diff --git a/samples/layouts/stepper/linear/src/LinearStepperFunctional.tsx b/samples/layouts/stepper/linear/src/LinearStepperFunctional.tsx new file mode 100644 index 0000000000..cb8891fa0b --- /dev/null +++ b/samples/layouts/stepper/linear/src/LinearStepperFunctional.tsx @@ -0,0 +1,151 @@ +import React, { useState, useRef, useCallback } from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; +import { + IgrStepper, + IgrStep, + IgrRadio, + IgrRadioGroup, + IgrButton, + IgrSwitch, + IgrCheckboxChangeEventArgs, + IgrInput, +} from 'igniteui-react'; +import 'igniteui-webcomponents/themes/light/bootstrap.css'; + +export default function LinearStepper() { + const stepperRef = useRef(null); + const infoFormRef = useRef(null); + const addressFormRef = useRef(null); + + const [linear, setLinear] = useState(false); + const [firstStepInvalid, setFirstStepInvalid] = useState(true); + const [secondStepInvalid, setSecondStepInvalid] = useState(true); + + const checkFormValidity = useCallback((form: React.RefObject): boolean => { + for (const element of Array.from(form.current?.children ?? [])) { + if ( + element.tagName.toLowerCase() === 'igc-input' && + (element as any).value === '' + ) { + const oldInvalid = (element as any).invalid; + const isElementInvalid = !(element as any).checkValidity(); + (element as any).invalid = oldInvalid; + if (isElementInvalid) return true; + } + } + return false; + }, []); + + const checkActiveStepValidity = useCallback((isLinear: boolean) => { + if (!isLinear) return; + const steps = stepperRef.current?.steps ?? []; + const activeIndex = steps.findIndex((s: any) => s.active); + if (activeIndex === 0) { + setFirstStepInvalid(checkFormValidity(infoFormRef)); + } else if (activeIndex === 1) { + setSecondStepInvalid(checkFormValidity(addressFormRef)); + } + }, [checkFormValidity]); + + const handleInput = useCallback(() => checkActiveStepValidity(linear), [linear, checkActiveStepValidity]); + + const onSwitchChange = useCallback((e: IgrCheckboxChangeEventArgs) => { + const isLinear = e.detail.checked; + setLinear(isLinear); + if (isLinear) { + checkActiveStepValidity(isLinear); + } + }, [checkActiveStepValidity]); + + return ( +
+ + Linear + + + + + Personal Info + + + + stepperRef.current?.next()} + > + NEXT + + + + + Delivery address +
+ + + stepperRef.current?.prev()}> + PREVIOUS + + stepperRef.current?.next()} + > + NEXT + + +
+ + Billing address + (optional) +
+ + + stepperRef.current?.prev()}> + PREVIOUS + + stepperRef.current?.next()}> + NEXT + + +
+ + Payment + + + PayPal (n@mail.com; 18/02/2021) + + + Visa (**** **** **** 1234; 12/23) + + + MasterCard (**** **** **** 5678; 12/24) + + + stepperRef.current?.prev()}> + PREVIOUS + + stepperRef.current?.next()}> + SUBMIT + + + + Delivery status +

+ Your order is on its way. Expect delivery on 25th September 2021. + Delivery address: San Jose, CA 94243. +

+ stepperRef.current?.prev()}> + PREVIOUS + + stepperRef.current?.reset()}> + RESET + +
+
+
+ ); +} + +// rendering above component to the React DOM +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render(); diff --git a/samples/layouts/tile-manager/actions/src/TileManagerActionsFunctional.tsx b/samples/layouts/tile-manager/actions/src/TileManagerActionsFunctional.tsx new file mode 100644 index 0000000000..d11588f55e --- /dev/null +++ b/samples/layouts/tile-manager/actions/src/TileManagerActionsFunctional.tsx @@ -0,0 +1,135 @@ +import React, { useCallback } from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; +import './layout.css'; +import { IgrTileManager, IgrTile, IgrIconButton, registerIconFromText } from 'igniteui-react'; +import 'igniteui-webcomponents/themes/light/bootstrap.css'; + +// Icon registration is a one-time module-level side effect (no state needed) +registerIconFromText( + 'north_east', + '', + 'material' +); +registerIconFromText( + 'south_west', + '', + 'material' +); +registerIconFromText( + 'more', + '', + 'material' +); +registerIconFromText( + 'chart', + '', + 'material' +); + +export default function Actions() { + // useCallback ensures stable handler references across renders + const onCustomOneClick = useCallback((event: React.MouseEvent) => { + const tile = (event.currentTarget as HTMLElement).closest('igc-tile') as any; + if (!tile) return; + + tile.maximized = !tile.maximized; + + const currentBtn = event.currentTarget as HTMLElement; + if (tile.maximized) { + currentBtn.setAttribute('name', 'south_west'); + currentBtn.setAttribute('aria-label', 'collapse'); + + const chartBtn = document.createElement('igc-icon-button'); + chartBtn.classList.add('additional-action'); + chartBtn.setAttribute('slot', 'actions'); + chartBtn.setAttribute('variant', 'flat'); + chartBtn.setAttribute('collection', 'material'); + chartBtn.setAttribute('name', 'chart'); + chartBtn.setAttribute('aria-label', 'chart'); + + const moreBtn = document.createElement('igc-icon-button'); + moreBtn.classList.add('additional-action'); + moreBtn.setAttribute('slot', 'actions'); + moreBtn.setAttribute('variant', 'flat'); + moreBtn.setAttribute('collection', 'material'); + moreBtn.setAttribute('name', 'more'); + moreBtn.setAttribute('aria-label', 'more'); + + tile.append(chartBtn); + tile.append(moreBtn); + } else { + currentBtn.setAttribute('name', 'north_east'); + currentBtn.setAttribute('aria-label', 'expand'); + + tile.querySelectorAll('.additional-action').forEach((btn: Element) => btn.remove()); + } + }, []); + + const onCustomTwoClick = useCallback((event: React.MouseEvent) => { + const tile = (event.currentTarget as HTMLElement).closest('igc-tile') as any; + if (!tile) return; + + tile.maximized = !tile.maximized; + + const currentBtn = event.currentTarget as HTMLElement; + if (tile.maximized) { + currentBtn.setAttribute('name', 'south_west'); + currentBtn.setAttribute('aria-label', 'collapse'); + } else { + currentBtn.setAttribute('name', 'north_east'); + currentBtn.setAttribute('aria-label', 'expand'); + } + }, []); + + return ( +
+ + +

Default Actions

+

This tile has default actions and title.

+
+ +

No Fullscreen Action

+

Fullscreen is disabled via property.

+
+ +

Custom Actions

+ +

Replace the default actions with custom ones, and include extra actions when the tile is maximized.

+
+ + +

Display only custom actions in the header.

+
+ +

Only title

+

Display only title in the header.

+
+ +

Content only.

+
+
+
+ ); +} + +// rendering above component to the React DOM +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render(); diff --git a/samples/maps/geo-map/binding-data-csv/src/MapBindingDataCSVFunctional.tsx b/samples/maps/geo-map/binding-data-csv/src/MapBindingDataCSVFunctional.tsx new file mode 100644 index 0000000000..bcb3a6341a --- /dev/null +++ b/samples/maps/geo-map/binding-data-csv/src/MapBindingDataCSVFunctional.tsx @@ -0,0 +1,115 @@ +import React, { useRef, useEffect, useCallback } from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; +import WorldUtils from './WorldUtils'; +import { IgrGeographicMapModule } from 'igniteui-react-maps'; +import { IgrGeographicMap } from 'igniteui-react-maps'; +import { IgrGeographicHighDensityScatterSeries } from 'igniteui-react-maps'; +import { IgrDataChartInteractivityModule } from 'igniteui-react-charts'; +import { IgrDataContext } from 'igniteui-react-core'; + +IgrGeographicMapModule.register(); +IgrDataChartInteractivityModule.register(); + +function createTooltip(context: any) { + const dataContext = context.dataContext as IgrDataContext; + if (!dataContext) return null; + + const series = dataContext.series as any; + if (!series) return null; + + const dataItem = dataContext.item as any; + if (!dataItem) return null; + + const lat = WorldUtils.toStringLat(dataItem.latitude); + const lon = WorldUtils.toStringLon(dataItem.longitude); + const population = WorldUtils.toStringAbbr(dataItem.population); + + return ( +
+
{dataItem.name}
+
+
+
Latitude:
+
{lat}
+
+
+
Longitude:
+
{lon}
+
+
+
Population:
+
{population}
+
+
+
+ ); +} + +export default function MapBindingDataCSV() { + const geoMapRef = useRef(null); + + const onDataLoaded = useCallback((csvData: string) => { + const geoMap = geoMapRef.current; + if (!geoMap) return; + + const csvLines = csvData.split('\n'); + + const geoLocations: any[] = []; + for (let i = 1; i < csvLines.length; i++) { + const columns = csvLines[i].split(','); + geoLocations.push({ + latitude: Number(columns[1]), + longitude: Number(columns[2]), + name: columns[0], + population: Number(columns[3]), + }); + } + + const geoSeries = new IgrGeographicHighDensityScatterSeries({ name: 'hdSeries' }); + geoSeries.dataSource = geoLocations; + geoSeries.latitudeMemberPath = 'latitude'; + geoSeries.longitudeMemberPath = 'longitude'; + geoSeries.heatMaximumColor = 'Red'; + geoSeries.heatMinimumColor = 'Black'; + geoSeries.heatMinimum = 0; + geoSeries.heatMaximum = 5; + geoSeries.pointExtent = 1; + geoSeries.tooltipTemplate = createTooltip; + geoSeries.mouseOverEnabled = true; + + geoMap.series.add(geoSeries); + + geoMap.zoomToGeographic({ + left: -130, + top: 15, + width: Math.abs(-130 + 65), + height: Math.abs(50 - 15), + }); + }, []); + + // useEffect replaces componentDidMount for side-effects like data fetching + useEffect(() => { + fetch('https://static.infragistics.com/xplatform/data/UsaCitiesPopulation.csv') + .then(response => response.text()) + .then(data => onDataLoaded(data)); + }, [onDataLoaded]); + + return ( +
+
+ +
+
Imagery Tiles: @OpenStreetMap
+
+ ); +} + +// rendering above component to the React DOM +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render(); diff --git a/samples/scheduling/calendar/disabled-dates/src/CalendarDisabledDatesFunctional.tsx b/samples/scheduling/calendar/disabled-dates/src/CalendarDisabledDatesFunctional.tsx new file mode 100644 index 0000000000..13f3b3cfe1 --- /dev/null +++ b/samples/scheduling/calendar/disabled-dates/src/CalendarDisabledDatesFunctional.tsx @@ -0,0 +1,27 @@ +import React, { useMemo } from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; +import { IgrCalendar, DateRangeDescriptor, DateRangeType } from 'igniteui-react'; +import 'igniteui-webcomponents/themes/light/bootstrap.css'; + +export default function CalendarDisabledDates() { + // useMemo ensures the disabled-dates array is created only once (stable reference) + const disabledDates = useMemo(() => { + const today = new Date(); + const range = [ + new Date(today.getFullYear(), today.getMonth(), 3), + new Date(today.getFullYear(), today.getMonth(), 8), + ]; + return [{ dateRange: range, type: DateRangeType.Specific }]; + }, []); + + return ( +
+ +
+ ); +} + +// rendering above component to the React DOM +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render();