anu-verzum 1.16.0 → 1.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,21 +1,35 @@
1
- <h1><strong><code>&lt;ANUVerzum /&gt;</code> JS</strong> FRAMEWORK USAGE:</h1>
1
+ <h1><strong><code>&lt;ANUVerzum /&gt;</code> JS</strong></h1>
2
2
 
3
3
  <br>
4
4
 
5
5
  <h3>@author: <strong>Anubis-programmer</strong></h3>
6
6
  <h3>@license: <strong>MIT</strong></h3>
7
+ <h3>@version: <strong>1.18.1</strong></h3>
7
8
 
8
9
  <br>
9
10
 
10
- <h2>Getting Started</h2>
11
+ A lightweight React-inspired UI library for building component-based web applications in JavaScript and TypeScript.
11
12
 
12
- <h3>Installation</h3>
13
+ - Fiber-based virtual DOM with time-sliced rendering (`requestIdleCallback`)
14
+ - Class components with full lifecycle support and function components
15
+ - JSX via a custom Babel preset — no separate TypeScript preset needed
16
+ - Redux-compatible state management with thunk middleware and memoized selectors
17
+ - Client-side routing over the History API
18
+ - Context API, i18n (Intl), feature flags, and built-in event analytics (Anulytics)
19
+ - Ships with TypeScript declaration files — no `@types` package needed
20
+
21
+ <br>
22
+ <hr>
23
+
24
+ <h2 id="getting-started">Getting Started</h2>
25
+
26
+ <h3 id="installation">Installation</h3>
13
27
 
14
28
  ```bash
15
29
  npm install anu-verzum
16
30
  ```
17
31
 
18
- <h3>Babel setup</h3>
32
+ <h3 id="babel-setup">Babel setup</h3>
19
33
 
20
34
  Create a `babel.config.json` in your project root:
21
35
 
@@ -28,14 +42,9 @@ Create a `babel.config.json` in your project root:
28
42
  }
29
43
  ```
30
44
 
31
- `anu-verzum/babel-preset` handles two things automatically:
32
-
33
- - Transforms JSX to `Anu.createElement()` calls, including the `<>...</>` fragment shorthand (mapped to `Anu.Fragment`).
34
- - Strips TypeScript syntax via `@babel/plugin-transform-typescript` with `jsxPragma: 'Anu'` pre-configured, so the `Anu` import is never incorrectly elided — even in files that only use host elements like `<div>` or `<span>`.
45
+ `anu-verzum/babel-preset` transforms JSX to `Anu.createElement()` calls and strips TypeScript syntax automatically. **Do not** add `@babel/preset-typescript` separately — running both would cause errors.
35
46
 
36
- Because the preset handles TypeScript stripping itself, **do not** add `@babel/preset-typescript` separately — running both would transform TypeScript twice and cause errors.
37
-
38
- <h3>Webpack setup</h3>
47
+ <h3 id="webpack-setup">Webpack setup</h3>
39
48
 
40
49
  Install the required build dependencies:
41
50
 
@@ -91,11 +100,9 @@ Add scripts to `package.json`:
91
100
  }
92
101
  ```
93
102
 
94
- <h3>Importing in your files</h3>
103
+ <h3 id="importing-in-your-files">Importing in your files</h3>
95
104
 
96
- Every file that contains JSX should import `Anu`, because the JSX transform expands to
97
- `Anu.createElement(...)` calls at compile time. The preset keeps this import in scope
98
- automatically, but it is still required at runtime:
105
+ Every file that contains JSX must import `Anu`, because the JSX transform expands to `Anu.createElement(...)` calls at compile time:
99
106
 
100
107
  ```javascript
101
108
  import Anu from 'anu-verzum';
@@ -107,7 +114,7 @@ const App = () => (
107
114
  Anu.render(<App />, document.getElementById('root'));
108
115
  ```
109
116
 
110
- <h3>TypeScript setup</h3>
117
+ <h3 id="typescript-setup">TypeScript setup</h3>
111
118
 
112
119
  The library ships with declaration files (`.d.ts`) out of the box — no `@types` package is needed.
113
120
 
@@ -148,62 +155,7 @@ Create `tsconfig.json`:
148
155
  | `skipLibCheck` | `true` | Skips type checking inside `node_modules` |
149
156
  | `moduleResolution` | `"bundler"` | Correct setting for Webpack/Babel projects |
150
157
 
151
- #### Build pipeline
152
-
153
- Compilation and type checking are intentionally separate:
154
-
155
- ```bash
156
- npm start # dev server — Babel compiles, always succeeds
157
- npx tsc --noEmit # type check only — surfaces type errors
158
- ```
159
-
160
- Because compilation goes through Babel, `npm start` and `npm run build` succeed regardless of type errors. Run `npx tsc --noEmit` during development to catch type issues without blocking the build.
161
-
162
- #### Typed class components
163
-
164
- Use the exported `Component<P, S>` base class to get fully-typed props and state:
165
-
166
- ```typescript
167
- import Anu, { Component, AnuElement } from 'anu-verzum';
168
-
169
- interface CounterProps {
170
- initialCount: number;
171
- }
172
-
173
- interface CounterState {
174
- count: number;
175
- }
176
-
177
- class Counter extends Component<CounterProps, CounterState> {
178
- constructor(props: CounterProps) {
179
- super(props);
180
- this.state = { count: props.initialCount };
181
- }
182
-
183
- render(): AnuElement {
184
- return (
185
- <div>
186
- <p>Count: {this.state.count}</p>
187
- <button onClick={() => this.setState({ count: this.state.count + 1 })}>
188
- Increment
189
- </button>
190
- </div>
191
- );
192
- }
193
- }
194
- ```
195
-
196
- #### Typed function components
197
-
198
- ```typescript
199
- import Anu, { AnuElement, Props } from 'anu-verzum';
200
-
201
- interface GreetingProps extends Props {
202
- name: string;
203
- }
204
-
205
- const Greeting = ({ name }: GreetingProps): AnuElement => <p>Hello, {name}!</p>;
206
- ```
158
+ Compilation and type checking are intentionally separate — `npm start` and `npm run build` succeed regardless of type errors. Run `npx tsc --noEmit` during development to catch type issues without blocking the build.
207
159
 
208
160
  #### Exported types
209
161
 
@@ -213,9 +165,9 @@ The following types are exported from `anu-verzum` for use in consumer projects:
213
165
  |------|-------------|
214
166
  | `AnuElement` | The virtual-DOM element descriptor (return type of `createElement`) |
215
167
  | `AnuChild` | Union of all valid JSX child types: `AnuElement \| string \| number \| boolean \| null \| undefined` |
216
- | `AnuNode` | Recursive child tree type used by `Props.children` — `AnuChild \| AnuNode[]`; accepts single children, arrays, and nested arrays (e.g. from `.map()`) |
217
- | `AnuCSSProperties` | Style object type used by `Props.style` — `Partial<Record<keyof CSSStyleDeclaration, string \| number>>`; allows numeric values for unitless CSS properties such as `flexShrink`, `zIndex`, `opacity` |
218
- | `Props` | Base props type all component prop objects should extend this |
168
+ | `AnuNode` | Recursive child tree type — `AnuChild \| AnuNode[]`; accepts single children, arrays, and nested arrays (e.g. from `.map()`). Can also be imported directly to type a `children` field in a plain `type`: `type MyProps = { children?: AnuNode; }` |
169
+ | `AnuCSSProperties` | Style object type used by `Props.style` — `Partial<Record<keyof CSSStyleDeclaration, string \| number>>` |
170
+ | `Props` | Base props type providing `children?: AnuNode`, `style?: AnuCSSProperties`, and an open index signature. Extend it when a component uses `children` or needs the flexible index signature; for simple prop shapes, a plain `type` alias works equally well |
219
171
  | `Ref<T>` | Reference object created by `Anu.createRef<T>()` |
220
172
  | `Component<P, S>` | Abstract base class for class components |
221
173
  | `FunctionComponent<P>` | Function component signature |
@@ -235,8 +187,6 @@ The following types are exported from `anu-verzum` for use in consumer projects:
235
187
 
236
188
  #### Library development scripts
237
189
 
238
- These scripts are available when working on the library itself:
239
-
240
190
  ```bash
241
191
  npm run clean # Delete dist/ entirely
242
192
  npm run build # Clean, compile TypeScript sources to dist/, and emit .d.ts files
@@ -248,2093 +198,11 @@ npm run format # Format all source files with Prettier
248
198
  <br>
249
199
  <hr>
250
200
 
251
- <h2>Framework Usage:</h2>
252
-
253
- <ul>
254
- <li>
255
- <a href="#creating-components-and-rendering">Creating components and rendering</a>
256
- </li>
257
- <ul>
258
- <li>
259
- <a href="#function-components">Function components</a>
260
- </li>
261
- <li>
262
- <a href="#class-based-components">Class-based components</a>
263
- </li>
264
- <li>
265
- <a href="#rendering-props">Rendering props</a>
266
- </li>
267
- <li>
268
- <a href="#wrapper-components-and-sub-components">Wrapper components and sub-components</a>
269
- </li>
270
- <li>
271
- <a href="#array-rendering">Array rendering</a>
272
- </li>
273
- <li>
274
- <a href="#avoiding-unnecessary-wrapper-elements">Avoiding unnecessary wrapper elements</a>
275
- </li>
276
- <li>
277
- <a href="#list-keys">Keys for dynamic lists</a>
278
- </li>
279
- <li>
280
- <a href="#refs">The refs</a>
281
- </li>
282
- <li>
283
- <a href="#rendering-application">Rendering application</a>
284
- </li>
285
- </ul>
286
- <li>
287
- <a href="#history-api">Routing - The History API</a>
288
- </li>
289
- <ul>
290
- <li>
291
- <a href="#linking-and-routing">Linking and routing</a>
292
- </li>
293
- <li>
294
- <a href="#redirecting-with-history-redirect">Redirecting with &lt;Anu.History.Redirect /&gt;</a>
295
- </li>
296
- <li>
297
- <a href="#redirecting-with-history-goto">Redirecting with Anu.History.goTo()</a>
298
- </li>
299
- <li>
300
- <a href="#reading-url-parameters">Reading URL parameters</a>
301
- </li>
302
- </ul>
303
- <li>
304
- <a href="#server-api">Calling the server asynchronously - The Server API</a>
305
- </li>
306
- <ul>
307
- <li>
308
- <a href="#get-and-delete-methods">GET and DELETE HTTP methods</a>
309
- </li>
310
- <li>
311
- <a href="#post-and-put-methods">POST and PUT HTTP methods</a>
312
- </li>
313
- <li>
314
- <a href="#file-method">FILE HTTP method</a>
315
- </li>
316
- </ul>
317
- <li>
318
- <a href="#anulytics-api">Tracking users - The Anulytics API</a>
319
- </li>
320
- <ul>
321
- <li>
322
- <a href="#anulytics-provider">Wrapping up your front-end project within the &lt;Anu.Anulytics.Provider /&gt; component</a>
323
- </li>
324
- <li>
325
- <a href="#anulytics-track-event">tracking events on elements using Anu.Anulytics.trackEvent(event, props)</a>
326
- </li>
327
- </ul>
328
- <li>
329
- <a href="#context-api">Creating and accessing the context out of the "normal props flow" - The Context API</a>
330
- </li>
331
- <ul>
332
- <li>
333
- <a href="#creating-context">Creating the context</a>
334
- </li>
335
- <li>
336
- <a href="#usage-of-context-provider-and-consumers">Usage of the context provider and its consumer(s)</a>
337
- </li>
338
- </ul>
339
- <li>
340
- <a href="#store-api">Storing and mutating the global state on actions dispatched, memoizing complex conversions on the global state and combining reducers - The Store API</a>
341
- </li>
342
- <ul>
343
- <li>
344
- <a href="#dispatching-actions">Dispatching actions</a>
345
- </li>
346
- <li>
347
- <a href="#handling-actions-with-reducers">Handling actions with reducers</a>
348
- </li>
349
- <li>
350
- <a href="#memoizing-state-conversions">Memoizing (global) state conversions</a>
351
- </li>
352
- <li>
353
- <a href="#combining-reducers">Combining reducers</a>
354
- </li>
355
- <li>
356
- <a href="#creating-the-store">Creating the store</a>
357
- </li>
358
- </ul>
359
- <li>
360
- <a href="#connector-api">Connecting components to the global state - The Connector API</a>
361
- </li>
362
- <ul>
363
- <li>
364
- <a href="#connect-to-store">Connect to the store</a>
365
- </li>
366
- <li>
367
- <a href="#create-container-component">Create container component</a>
368
- </li>
369
- </ul>
370
- <li>
371
- <a href="#intl-api">Supporting multiple languages - The Intl API</a>
372
- </li>
373
- <ul>
374
- <li>
375
- <a href="#creating-language-objects">Creating the supported language objects</a>
376
- </li>
377
- <li>
378
- <a href="#adding-language-objects-to-provider">Adding the language objects to the Intl provider</a>
379
- </li>
380
- <li>
381
- <a href="#formatting-component-texts">Formatting component texts</a>
382
- </li>
383
- <li>
384
- <a href="#formatting-attribute-texts">Formatting attribute texts</a>
385
- </li>
386
- <li>
387
- <a href="#abbreviating-numbers">Abbreviating numbers</a>
388
- </li>
389
- </ul>
390
- <li>
391
- <a href="#feature-api">Switching features on / off - The Feature API</a>
392
- </li>
393
- <ul>
394
- <li>
395
- <a href="#setting-features-list">Setting the features list</a>
396
- </li>
397
- <li>
398
- <a href="#toggling-features">Toggling the features</a>
399
- </li>
400
- </ul>
401
- </ul>
402
-
403
- <br>
201
+ <h2>Documentation</h2>
404
202
 
405
- <h2>Utilities:</h2>
203
+ Full usage documentation, API reference, and code examples:
406
204
 
407
- <ul>
408
- <li>
409
- <a href="#deep-equal">Deep equality check for objects using Anu.Utils.deepEqual()</a>
410
- </li>
411
- </ul>
205
+ **[USERS_MANUAL.md](./documentation/USERS_MANUAL.md)**
412
206
 
413
207
  <br>
414
208
  <hr>
415
-
416
- <h1><strong><code>&lt;ANUVerzum /&gt;</code> JS</strong> USAGE</h1>
417
-
418
- <br>
419
-
420
- <h2 id="creating-components-and-rendering">Creating components and rendering elements</h2>
421
-
422
- Supports both <i>HTML</i> and <i>inline-SVG</i> element creation, "stateful" (or class-based) components and function (currently "stateless") components!<br>
423
- - If you wish to use the SVG <code>&lt;a&gt;</code>, <code>&lt;style&gt;</code> or <code>&lt;title&gt;</code> tags, please use instead <code>&lt;anchor&gt;</code>, <code>&lt;svgStyle&gt;</code> or <code>&lt;svgTitle&gt;</code> respectively)!
424
- - <code>&lt;script&gt;</code> SVG tag is not supported! Please create a separate component for the elements and define their behavior as you would do in case of a regular component!
425
- - When creating <code>&lt;svg&gt;</code> tag, you don't have to define the <code>xmlns</code> attribute.
426
- - Adding style definition inside <code>&lt;svgStyle&gt;</code> should be done as a string template as <code>children</code>, e.g.:
427
-
428
- ```typescript
429
- <svg viewBox="0 0 10 10">
430
- <svgStyle>
431
- {`circle {
432
- fill: gold;
433
- stroke: maroon;
434
- stroke-width: 2px;
435
- }`}
436
- </svgStyle>
437
- <circle cx="5" cy="5" r="4" />
438
- </svg>
439
- ```
440
-
441
- <h3 id="function-components">Function components</h3>
442
-
443
- - Function components are functions which can receive <code>props</code> and must always return either an <i>HTML element</i>, an <i>inline-SVG element</i>, a <i>Component</i> or <code>NULL</code>.
444
- - Both class-based and function component names must start with a capital letter, inline-SVG-s and HTML element names are lower-case!
445
-
446
- ```typescript
447
- import Anu, { Props, AnuElement } from 'anu-verzum';
448
-
449
- const FunctionComponent = (props: Props): AnuElement => {
450
- // ...
451
- return (
452
- <HTMLElement_inlineSVGElement_or_Component />
453
- );
454
- };
455
- ```
456
-
457
- <strong>Example</strong> - <i>returning HTML elements</i>:
458
-
459
- ```typescript
460
- interface TitleBarProps extends Props {
461
- title: string;
462
- }
463
-
464
- const MyTitleBar = ({ title }: TitleBarProps): AnuElement => (
465
- <div>
466
- <h1>{title}</h1>
467
- </div>
468
- );
469
- ```
470
-
471
- <strong>Example</strong> - <i>returning inline-SVG elements</i>:
472
-
473
- ```typescript
474
- const MyCircle = (): AnuElement => (
475
- <svg viewBox="0 0 10 10">
476
- <svgStyle>
477
- {`circle {
478
- fill: gold;
479
- stroke: maroon;
480
- stroke-width: 2px;
481
- }`}
482
- </svgStyle>
483
- <circle cx="5" cy="5" r="4" />
484
- </svg>
485
- );
486
- ```
487
-
488
- <strong>Example</strong> - <i>returning composed elements</i>:
489
-
490
- ```typescript
491
- const TitleWithCircle = (): AnuElement => (
492
- <div>
493
- <MyTitleBar title="Hello ANUVerzum!" />
494
- <MyCircle />
495
- </div>
496
- );
497
- ```
498
-
499
- <h3 id="class-based-components">Class-based components</h3>
500
-
501
- - Always must extend <code>Anu.Component</code>, always must implement <code>render()</code> method!
502
- - If the component receives <code>props</code> and you want to do additional settings inside the constructor (e.g. binding handlers or setting up initial state),
503
- <code>super(props)</code> should always be the first call inside the <code>constructor(props)</code>!
504
- - You can use <code>setState()</code> to re-render the component.
505
- It takes <strong>ONE</strong> argument which can either be:
506
- - A <code>state</code> object which will be merged with the old state
507
- - A <code>setStateCallback</code> function which takes the actual <code>state</code> and <code>props</code> as arguments (useful if <code>state</code> and <code>props</code> were updated asynchronously) and returns the new <code>state</code> value.
508
-
509
- ```typescript
510
- type SetStateCallbackType<S, P> = (prevState: S, props: P) => S;
511
- setState(partialState: Partial<S> | SetStateCallbackType<S, P>): void;
512
- ```
513
-
514
- - <strong>NEVER</strong> use <code>setState()</code> in <code>constructor()</code> or <code>componentWillUnmount()</code>!
515
- - The <code>setState()</code> can be called in <code>componentDidMount()</code> and <code>componentDidUpdate()</code>, but only in condition - as you would do in React
516
-
517
- ```typescript
518
- import Anu, { Props, AnuElement, Component } from 'anu-verzum';
519
-
520
- interface MyProps extends Props {
521
- // declare your props here
522
- }
523
-
524
- interface MyState {
525
- // declare your state shape here
526
- }
527
-
528
- class ClassComponent extends Component<MyProps, MyState> {
529
- constructor(props: MyProps) {
530
- super(props);
531
- // ALWAYS bind methods if passed down to component / HTML element because of "this" keyword:
532
- this.customMethodName = this.customMethodName.bind(this);
533
- // Set initial state:
534
- this.state = { /* ... */ };
535
- }
536
- // Lifecycle-methods:
537
- componentDidMount(): void {
538
- /**
539
- * - Will be called after mounting - if component is inserted in the tree within this loop
540
- * - Call "setState()"
541
- * - Set up subscriptions -- Destroy them in "componentWillUnmount()"!
542
- * - Instantiate network request (load data)
543
- */
544
- }
545
- componentDidUpdate(prevProps: MyProps, prevState: MyState): void {
546
- /**
547
- * - Will be called after mounting - if component is updated within this loop
548
- * - Call "setState()" -- Only in condition!
549
- */
550
- }
551
- componentWillUnmount(): void {
552
- /**
553
- * - Will be called before component is removed from the tree
554
- * - Perform necessary cleanup -- destroy subscriptions set in "componentDidMount()"!
555
- */
556
- }
557
- // Custom method:
558
- customMethodName(): void {
559
- /**
560
- * - "setState()" can be used.
561
- * - Possible params for "setState()":
562
- * - partialState -- object | callback -- The new state object or a callback with params "prevState" and "prevProps"
563
- */
564
- }
565
- render(): AnuElement {
566
- return (
567
- <HTMLElement_inline-SVGElement_or_Component
568
- onCustomEvent={this.customMethodName}
569
- />
570
- );
571
- }
572
- }
573
- ```
574
-
575
- <h3 id="rendering-props">Rendering props</h3>
576
-
577
- - To render props, store an HTML element, inline-SVG element or component in a variable and when rendering, insert it, e.g.:
578
-
579
- ```typescript
580
- interface TextProps extends Props {
581
- text: string;
582
- }
583
-
584
- const PropsRenderer1 = ({ children }: Props): AnuElement => <div>{children}</div>;
585
- // Short syntax:
586
- const PropsRenderer2 = ({ text }: TextProps): AnuElement => <p>{text}</p>;
587
- ```
588
-
589
- <h3 id="wrapper-components-and-sub-components">Wrapper components and related sub-components</h3>
590
-
591
- - The approach is the following:
592
- - First, create a wrapper component which will render its <code>props.children</code>, for example.
593
- - Then, create wrapped / sub-components.
594
- - Add the sub-component to the wrapper as a property.
595
-
596
- ```typescript
597
- // Declare the wrapper type so TypeScript knows about the .Elem sub-component:
598
- interface ElemWrapperComponent {
599
- (props: Props): AnuElement;
600
- Elem: (props: Props) => AnuElement;
601
- }
602
-
603
- // Wrapper element
604
- const ElemWrapper = (({ children }: Props): AnuElement => <div>{children}</div>) as ElemWrapperComponent;
605
- // Sub-component
606
- ElemWrapper.Elem = ({ children }: Props): AnuElement => <p>{children}</p>;
607
- // Usage:
608
- <ElemWrapper>
609
- <ElemWrapper.Elem>
610
- Lorem Ipsum Dolor...
611
- </ElemWrapper.Elem>
612
- <ElemWrapper.Elem>
613
- Lorem Ipsum Dolor...
614
- </ElemWrapper.Elem>
615
- </ElemWrapper>
616
- ```
617
-
618
- <h3 id="array-rendering">Rendering array</h3>
619
-
620
- - When rendering array, you don't return a nested structure but rather an array of elements / components, e.g.:
621
-
622
- ```typescript
623
- interface ParagraphsProps extends Props {
624
- firstParagraph: string;
625
- secondParagraph: string;
626
- }
627
-
628
- const ArrayRenderer = ({ firstParagraph, secondParagraph }: ParagraphsProps): AnuElement[] => [
629
- <p>{firstParagraph}</p>,
630
- <p>{secondParagraph}</p>
631
- ];
632
-
633
- const ArrayRendererWrapper = ({ firstParagraph, secondParagraph }: ParagraphsProps): AnuElement => (
634
- <div>
635
- <ArrayRenderer
636
- firstParagraph={firstParagraph}
637
- secondParagraph={secondParagraph}
638
- />
639
- </div>
640
- );
641
- ```
642
-
643
- <h3 id="avoiding-unnecessary-wrapper-elements">Avoiding unnecessary wrapper elements</h3>
644
-
645
- - It is useful when you have a list of properties you want to loop through and render them (i.e. not in a nested structure but rather one after the other) but you don't want an extra <code>&lt;div /&gt;</code> element to be rendered.
646
- - Both syntaxes below are equivalent — use whichever you prefer:
647
- - <code>&lt;Anu.Fragment&gt;...&lt;/Anu.Fragment&gt;</code> — explicit form
648
- - <code>&lt;&gt;...&lt;/&gt;</code> — shorthand form (available when using the <code>anu-verzum/babel-preset</code>)
649
-
650
- ```typescript
651
- interface ListProps extends Props {
652
- somethingToLoop: string[];
653
- }
654
-
655
- // Explicit form:
656
- const ElemList = ({ somethingToLoop }: ListProps): AnuElement => (
657
- <Anu.Fragment>
658
- {somethingToLoop.map((prop, i) => <li key={`list-item-${i}`}>{prop}</li>)}
659
- </Anu.Fragment>
660
- );
661
-
662
- // Shorthand form — identical result:
663
- const ElemList = ({ somethingToLoop }: ListProps): AnuElement => (
664
- <>
665
- {somethingToLoop.map((prop, i) => <li key={`list-item-${i}`}>{prop}</li>)}
666
- </>
667
- );
668
-
669
- // Usage:
670
- const OrderedElemList = (): AnuElement => {
671
- const somethingToLoop: string[] = ['Coffee', 'Tea', 'Pálinka'];
672
- return (
673
- <div>
674
- <h4>Ordered list:</h4>
675
- <ol>
676
- <ElemList somethingToLoop={somethingToLoop} />
677
- </ol>
678
- </div>
679
- );
680
- };
681
- ```
682
-
683
- <h3 id="list-keys">Keys for dynamic lists</h3>
684
-
685
- - Add a <code>key</code> prop to each element in a dynamically rendered list. Keys allow the reconciler to match elements across renders by identity rather than position, so items that move, are inserted, or are removed do not cause unrelated siblings to lose their state or be recreated unnecessarily.
686
- - A key must be unique among siblings. Use a stable, unique identifier from your data (such as a database ID). Only fall back to the array index when the list never reorders and items are never inserted in the middle.
687
-
688
- ```typescript
689
- interface Item {
690
- id: string;
691
- label: string;
692
- }
693
-
694
- interface ItemListProps extends Props {
695
- items: Item[];
696
- }
697
-
698
- // Stable IDs — preferred:
699
- const ItemList = ({ items }: ItemListProps): AnuElement => (
700
- <ul>
701
- {items.map(({ id, label }) => (
702
- <li key={id}>{label}</li>
703
- ))}
704
- </ul>
705
- );
706
-
707
- // Index fallback — only when order never changes:
708
- const StaticList = ({ items }: ItemListProps): AnuElement => (
709
- <ul>
710
- {items.map(({ label }, i) => (
711
- <li key={`list-item-${i}`}>{label}</li>
712
- ))}
713
- </ul>
714
- );
715
- ```
716
-
717
- <h3 id="refs">The refs</h3>
718
-
719
- - In most cases, there is no need to use refs to manage (HTML) sub-components from the parent (class) component.
720
- However, there are some cases
721
- (e.g. focusing an input element after it has been mounted,
722
- uploading files using a custom uploader button,
723
- handling onclick event outside of a given element, etc.)
724
- when you want to imperatively modify a child outside of the typical dataflow.
725
- - Refs can <strong>ONLY BE CREATED IN CLASS COMPONENTS</strong> but can be passed as prop - use different name then!
726
- - Set <code>ref</code> attribute / property <strong>ONLY ON HOST COMPONENT</strong>!
727
-
728
- <strong>Example</strong> - <i>focusing an input element</i>:
729
-
730
- ```typescript
731
- import Anu, { Props, AnuElement, Component, Ref } from 'anu-verzum';
732
-
733
- class RefTestClass extends Component {
734
- input: Ref<HTMLInputElement>;
735
-
736
- constructor(props: Props) {
737
- super(props);
738
- this.input = Anu.createRef<HTMLInputElement>();
739
- }
740
- componentDidMount(): void {
741
- this.input.current!.focus();
742
- }
743
- render(): AnuElement {
744
- return (
745
- <label>
746
- <input ref={this.input} />
747
- </label>
748
- );
749
- }
750
- }
751
- ```
752
-
753
- <strong>Example</strong> - <i>file uploader with preview</i>:
754
-
755
- ```typescript
756
- interface FilePreview {
757
- name: string;
758
- file: File;
759
- }
760
-
761
- interface FileUploaderState {
762
- files: FilePreview[];
763
- }
764
-
765
- class FileUploader extends Component<Props, FileUploaderState> {
766
- fileInput: Ref<HTMLInputElement>;
767
-
768
- constructor(props: Props) {
769
- super(props);
770
- this.fileInput = Anu.createRef<HTMLInputElement>();
771
- this.state = {
772
- files: []
773
- };
774
- this.uploadFiles = this.uploadFiles.bind(this);
775
- this.handleFileUploaderClick = this.handleFileUploaderClick.bind(this);
776
- }
777
- handleFileUploaderClick(): void {
778
- this.fileInput.current!.click();
779
- }
780
- uploadFiles({ target }: { target: HTMLInputElement }): void {
781
- const nextState: FileUploaderState = { files: [] };
782
- for (let i = 0; i < target.files!.length; i++) {
783
- nextState.files.push({
784
- // Creates a name for preview.
785
- // Use it as 'src' attribute value in image:
786
- name: URL.createObjectURL(target.files![i]),
787
- file: target.files![i]
788
- });
789
- }
790
- this.setState(nextState);
791
- }
792
- render(): AnuElement {
793
- const { files } = this.state;
794
- return (
795
- <Anu.Fragment>
796
- <button
797
- className="my-fancy-uploader-button"
798
- onClick={this.handleFileUploaderClick}
799
- >
800
- Feltöltés
801
- </button>
802
- <input
803
- type="file"
804
- ref={this.fileInput}
805
- multiple
806
- onChange={this.uploadFiles}
807
- style={{ display: 'none' }}
808
- />
809
- <ul>
810
- {files.length > 0 && files.map(({ name, file }) => (
811
- <li key={name}>
812
- <img src={name} alt={`Uploaded file name: ${file.name}, type: ${file.type}, size: ${file.size}`} />
813
- </li>
814
- ))}
815
- </ul>
816
- </Anu.Fragment>
817
- );
818
- }
819
- }
820
- ```
821
-
822
- <strong>Example</strong> - <i>handling <code>onClick</code> event outside of the referenced element</i>:
823
-
824
- ```typescript
825
- interface ClickOutsideState {
826
- show: boolean;
827
- }
828
-
829
- class ClickOutsideComponent extends Component<Props, ClickOutsideState> {
830
- myRef: Ref<HTMLDivElement>;
831
-
832
- constructor(props: Props) {
833
- super(props);
834
- this.state = { show: true };
835
- this.myRef = Anu.createRef<HTMLDivElement>();
836
- this.handleClickOutside = this.handleClickOutside.bind(this);
837
- }
838
- componentDidMount(): void {
839
- document.addEventListener('mousedown', this.handleClickOutside);
840
- }
841
- componentWillUnmount(): void {
842
- document.removeEventListener('mousedown', this.handleClickOutside);
843
- }
844
- handleClickOutside({ target }: MouseEvent): void {
845
- if (this.myRef.current && !this.myRef.current.contains(target as Node)) {
846
- this.setState({ show: false });
847
- }
848
- }
849
- render(): AnuElement | null {
850
- const { show } = this.state;
851
- return show ? (
852
- <div ref={this.myRef}>
853
- Content will be hidden after you click outside of this box
854
- </div>
855
- ) : null;
856
- }
857
- }
858
- ```
859
-
860
- <strong>Example</strong> - <i>drag-and-drop todo list</i>:
861
-
862
- ```typescript
863
- const TODO_STATUS = {
864
- PENDING: 'pending' as const,
865
- COMPLETED: 'completed' as const
866
- };
867
-
868
- type TodoStatus = typeof TODO_STATUS[keyof typeof TODO_STATUS];
869
-
870
- interface TodoItem {
871
- name: string;
872
- status: TodoStatus;
873
- }
874
-
875
- interface DraggableTodoListState {
876
- todoList: TodoItem[];
877
- }
878
-
879
- class DraggableTodoList extends Component<Props, DraggableTodoListState> {
880
- constructor(props: Props) {
881
- super(props);
882
- this.state = {
883
- todoList: [
884
- { name: 'Coding', status: TODO_STATUS.PENDING },
885
- { name: 'German course', status: TODO_STATUS.PENDING },
886
- { name: 'Mathematics course', status: TODO_STATUS.PENDING },
887
- { name: 'Training', status: TODO_STATUS.PENDING },
888
- { name: 'Physics course', status: TODO_STATUS.COMPLETED }
889
- ]
890
- };
891
- this.handleDragStart = this.handleDragStart.bind(this);
892
- this.handleDragOver = this.handleDragOver.bind(this);
893
- this.handleDrop = this.handleDrop.bind(this);
894
- }
895
- handleDragStart({ dataTransfer }: DragEvent, taskName: string): void {
896
- dataTransfer!.setData('id', taskName);
897
- }
898
- handleDragOver(event: DragEvent): void {
899
- event.preventDefault();
900
- }
901
- handleDrop({ dataTransfer }: DragEvent, status: TodoStatus): void {
902
- const { todoList } = this.state;
903
- const id = dataTransfer!.getData('id');
904
- const list = todoList.map(task => {
905
- if (task.name === id) {
906
- task.status = status;
907
- }
908
- return task;
909
- });
910
- this.setState({ todoList: list });
911
- }
912
- render(): AnuElement {
913
- const { todoList } = this.state;
914
- const todoStatus = {
915
- [TODO_STATUS.PENDING]: [],
916
- [TODO_STATUS.COMPLETED]: []
917
- };
918
- todoList.forEach(task => {
919
- todoStatus[task.status].push(
920
- <div
921
- key={task.name}
922
- draggable
923
- className="todoListItem"
924
- onDragStart={event => this.handleDragStart(event, task.name)}
925
- >
926
- {task.name}
927
- </div>
928
- );
929
- });
930
- return (
931
- <div className="todoListContainer">
932
- <div
933
- className="todoListContent"
934
- onDragOver={this.handleDragOver}
935
- onDrop={event => this.handleDrop(event, TODO_STATUS.PENDING)}
936
- >
937
- <h4>
938
- <Anu.Intl.FormattedMessage id="proba.task.pending" defaultMessage="proba.task.pending" />:
939
- </h4>
940
- {todoStatus[TODO_STATUS.PENDING]}
941
- </div>
942
- <div
943
- className="todoListContent"
944
- onDragOver={this.handleDragOver}
945
- onDrop={event => this.handleDrop(event, TODO_STATUS.COMPLETED)}
946
- >
947
- <h4>
948
- <Anu.Intl.FormattedMessage id="proba.task.completed" defaultMessage="proba.task.completed" />:
949
- </h4>
950
- {todoStatus[TODO_STATUS.COMPLETED]}
951
- </div>
952
- </div>
953
- );
954
- }
955
- }
956
- ```
957
-
958
- <strong>Example</strong> - <i>infinite scroll</i>:
959
-
960
- ```typescript
961
- interface Photo {
962
- url: string;
963
- albumId: number;
964
- }
965
-
966
- interface PhotosResponse {
967
- data: Photo[];
968
- }
969
-
970
- interface ScrollComponentState {
971
- photos: Photo[];
972
- loading: boolean;
973
- page: number;
974
- prevY: number;
975
- }
976
-
977
- class ScrollComponent extends Component<Props, ScrollComponentState> {
978
- loadingRef: Ref<HTMLDivElement>;
979
- observer?: IntersectionObserver;
980
-
981
- constructor(props: Props) {
982
- super(props);
983
- this.state = { photos: [], loading: false, page: 0, prevY: 0 };
984
- this.loadingRef = Anu.createRef<HTMLDivElement>();
985
- this.handleObserver = this.handleObserver.bind(this);
986
- this.getPhotos = this.getPhotos.bind(this);
987
- }
988
- getPhotos(page: number): void {
989
- this.setState({ loading: true });
990
- Anu
991
- .ServerAPI
992
- .get<PhotosResponse>('/app/my/server/url', { page, limit: 10 })
993
- .then(({ response }) => {
994
- const photos = response?.data ?? [];
995
- this.setState({ photos: [...this.state.photos, ...photos] });
996
- this.setState({ loading: false });
997
- });
998
- }
999
- componentDidMount(): void {
1000
- this.getPhotos(this.state.page);
1001
- const options: IntersectionObserverInit = {
1002
- root: null,
1003
- rootMargin: '0px',
1004
- threshold: 1.0
1005
- };
1006
- this.observer = new IntersectionObserver(this.handleObserver, options);
1007
- this.observer.observe(this.loadingRef.current!);
1008
- }
1009
- handleObserver(entities: IntersectionObserverEntry[]): void {
1010
- const y = entities[0].boundingClientRect.y;
1011
- if (this.state.prevY > y) {
1012
- const lastPhoto = this.state.photos[this.state.photos.length - 1];
1013
- const curPage = lastPhoto.albumId;
1014
- this.getPhotos(curPage);
1015
- this.setState({ page: curPage });
1016
- }
1017
- this.setState({ prevY: y });
1018
- }
1019
- render(): AnuElement {
1020
- const loadingCSS = { height: '100px', margin: '30px' };
1021
- const loadingTextCSS = { display: this.state.loading ? 'block' : 'none' };
1022
- return (
1023
- <div className="container">
1024
- <div style={{ minHeight: '800px' }}>
1025
- {this.state.photos.map(user => (
1026
- <img key={user.url} src={user.url} height="100px" width="200px" />
1027
- ))}
1028
- </div>
1029
- <div ref={this.loadingRef} style={loadingCSS}>
1030
- <span style={loadingTextCSS}>Loading...</span>
1031
- </div>
1032
- </div>
1033
- );
1034
- }
1035
- }
1036
- ```
1037
-
1038
- <strong>Example</strong> - <i>laser pointer</i>:
1039
-
1040
- ```typescript
1041
- const canvasStyle: Partial<CSSStyleDeclaration> = {
1042
- height: '500px',
1043
- width: '100%',
1044
- border: '1px solid black'
1045
- };
1046
- const laserPointerStyle: Partial<CSSStyleDeclaration> = {
1047
- position: 'absolute',
1048
- backgroundColor: 'pink',
1049
- borderRadius: '50%',
1050
- opacity: '0.6',
1051
- pointerEvents: 'none',
1052
- left: '-20px',
1053
- top: '-20px',
1054
- width: '40px',
1055
- height: '40px',
1056
- zIndex: '10'
1057
- };
1058
-
1059
- interface LaserPointerState {
1060
- layerX: number;
1061
- layerY: number;
1062
- }
1063
-
1064
- class LaserPointer extends Component<Props, LaserPointerState> {
1065
- constructor(props: Props) {
1066
- super(props);
1067
- this.state = { layerX: 0, layerY: 0 };
1068
- this.handleMove = this.handleMove.bind(this);
1069
- }
1070
- handleMove({ layerX, layerY }: MouseEvent & { layerX: number; layerY: number }): void {
1071
- this.setState({ layerX, layerY });
1072
- }
1073
- render(): AnuElement {
1074
- const { layerX, layerY } = this.state;
1075
- return (
1076
- <div style={canvasStyle} onMouseMove={this.handleMove}>
1077
- <div
1078
- id="laser-pointer"
1079
- style={{
1080
- ...laserPointerStyle,
1081
- transform: `translate(${layerX}px, ${layerY}px)`
1082
- }}
1083
- />
1084
- </div>
1085
- );
1086
- }
1087
- }
1088
- ```
1089
-
1090
- <strong>Example</strong> - <i>paint application with preview (image can be saved as PNG) with <code>canvas</code> API</i>:
1091
-
1092
- ```typescript
1093
- const canvasHeight = 500;
1094
- const canvasWidth = 975;
1095
- const canvasContainer = {
1096
- display: 'flex',
1097
- flexDirection: 'column',
1098
- justifyContent: 'flex-start',
1099
- width: '100%',
1100
- alignItems: 'center'
1101
- };
1102
- const drawArea = {
1103
- width: '100%',
1104
- height: '552px',
1105
- border: '1px solid #808080',
1106
- position: 'relative',
1107
- backgroundColor: 'white'
1108
- };
1109
- const canvasMenuStyle = {
1110
- width: '650px',
1111
- height: '50px',
1112
- display: 'flex',
1113
- justifyContent: 'space-evenly',
1114
- borderRadius: '5px',
1115
- alignItems: 'center',
1116
- backgroundColor: '#a3a3a32d',
1117
- margin: '0 auto'
1118
- };
1119
- const canvas = {
1120
- height: `${canvasHeight}px`,
1121
- width: `${canvasWidth}px`,
1122
- cursor: 'crosshair'
1123
- };
1124
- const imgStyle = {
1125
- height: `${canvasHeight}px`,
1126
- width: `${canvasWidth}px`,
1127
- border: '1px solid black'
1128
- };
1129
- const BRUSH_MODES = {
1130
- PEN: 'Pen' as const,
1131
- ERASER: 'Eraser' as const
1132
- };
1133
-
1134
- type BrushMode = typeof BRUSH_MODES[keyof typeof BRUSH_MODES];
1135
-
1136
- interface CanvasState {
1137
- isDrawing: boolean;
1138
- lineWidth: number;
1139
- lineColor: string;
1140
- lineOpacity: number;
1141
- brushMode: BrushMode;
1142
- dataUrl?: string;
1143
- }
1144
-
1145
- class Canvas extends Component<Props, CanvasState> {
1146
- canvasRef: Ref<HTMLCanvasElement>;
1147
- ctxRef: Ref<CanvasRenderingContext2D>;
1148
- imageRef: Ref<HTMLImageElement>;
1149
-
1150
- constructor(props: Props) {
1151
- super(props);
1152
- this.state = {
1153
- isDrawing: false,
1154
- lineWidth: 1,
1155
- lineColor: 'black',
1156
- lineOpacity: 1,
1157
- brushMode: BRUSH_MODES.PEN,
1158
- dataUrl: undefined
1159
- };
1160
- this.canvasRef = Anu.createRef<HTMLCanvasElement>();
1161
- this.ctxRef = Anu.createRef<CanvasRenderingContext2D>();
1162
- this.imageRef = Anu.createRef<HTMLImageElement>();
1163
- this._reRender = this._reRender.bind(this);
1164
- this.startDrawing = this.startDrawing.bind(this);
1165
- this.endDrawing = this.endDrawing.bind(this);
1166
- this.draw = this.draw.bind(this);
1167
- this.setLineWidth = this.setLineWidth.bind(this);
1168
- this.setLineColor = this.setLineColor.bind(this);
1169
- this.setOpacity = this.setOpacity.bind(this);
1170
- this.setBrushMode = this.setBrushMode.bind(this);
1171
- this.handleSave = this.handleSave.bind(this);
1172
- }
1173
- _reRender(): void {
1174
- const { lineWidth, lineColor, lineOpacity } = this.state;
1175
- const canvasContext = this.canvasRef.current?.getContext('2d');
1176
- if (!canvasContext) return;
1177
- canvasContext.lineCap = "round";
1178
- canvasContext.lineJoin = "round";
1179
- canvasContext.globalAlpha = lineOpacity;
1180
- canvasContext.strokeStyle = lineColor;
1181
- canvasContext.lineWidth = lineWidth;
1182
- this.ctxRef.current = canvasContext;
1183
- }
1184
- componentDidMount(): void {
1185
- this._reRender();
1186
- }
1187
- componentDidUpdate(): void {
1188
- this._reRender();
1189
- }
1190
- startDrawing(event: MouseEvent): void {
1191
- const rect = (event.target as HTMLCanvasElement).getBoundingClientRect();
1192
- const x = event.clientX - rect.left;
1193
- const y = event.clientY - rect.top;
1194
- this.ctxRef.current!.beginPath();
1195
- this.ctxRef.current!.moveTo(x, y);
1196
- this.setState({ isDrawing: true });
1197
- }
1198
- endDrawing(): void {
1199
- this.setState({ isDrawing: false });
1200
- }
1201
- draw(event: MouseEvent): void {
1202
- const { lineWidth, brushMode, isDrawing } = this.state;
1203
- if (!isDrawing) return;
1204
- const rect = (event.target as HTMLCanvasElement).getBoundingClientRect();
1205
- const x = event.clientX - rect.left;
1206
- const y = event.clientY - rect.top;
1207
- if (brushMode === BRUSH_MODES.PEN) {
1208
- this.ctxRef.current!.lineTo(x, y);
1209
- this.ctxRef.current!.stroke();
1210
- } else {
1211
- this.ctxRef.current!.clearRect(x - lineWidth / 2, y - lineWidth / 2, lineWidth, lineWidth);
1212
- }
1213
- }
1214
- setLineWidth({ target }: { target: HTMLInputElement }): void {
1215
- this.setState({ lineWidth: Number(target.value) });
1216
- }
1217
- setLineColor({ target }: { target: HTMLInputElement }): void {
1218
- this.setState({ lineColor: target.value });
1219
- }
1220
- setOpacity({ target }: { target: HTMLInputElement }): void {
1221
- this.setState({ lineOpacity: Number(target.value) / 100 });
1222
- }
1223
- setBrushMode(): void {
1224
- this.setState(prevState => ({
1225
- ...prevState,
1226
- brushMode: prevState.brushMode === BRUSH_MODES.PEN ? BRUSH_MODES.ERASER : BRUSH_MODES.PEN
1227
- }));
1228
- }
1229
- handleSave(): void {
1230
- const dataUrl = this.canvasRef.current!.toDataURL();
1231
- this.setState({ dataUrl });
1232
- }
1233
- render(): AnuElement {
1234
- const {
1235
- lineWidth,
1236
- lineColor,
1237
- lineOpacity,
1238
- brushMode,
1239
- dataUrl
1240
- } = this.state;
1241
- return (
1242
- <div style={canvasContainer}>
1243
- <h1>Paint App</h1>
1244
- <div style={drawArea}>
1245
- <div style={canvasMenuStyle}>
1246
- <label>
1247
- Brush Color
1248
- <input
1249
- type="color"
1250
- value={lineColor}
1251
- onChange={this.setLineColor}
1252
- />
1253
- </label>
1254
- <label>
1255
- Brush Width
1256
- <input
1257
- type="range"
1258
- min="1"
1259
- max="20"
1260
- value={lineWidth}
1261
- onChange={this.setLineWidth}
1262
- />
1263
- </label>
1264
- <label>
1265
- Brush Opacity
1266
- <input
1267
- type="range"
1268
- min="1"
1269
- max="100"
1270
- value={lineOpacity * 100}
1271
- onChange={this.setOpacity}
1272
- />
1273
- </label>
1274
- <label>
1275
- Selected brush type:
1276
- <button onClick={this.setBrushMode}>
1277
- {brushMode}
1278
- </button>
1279
- </label>
1280
- <button onClick={this.handleSave}>Save</button>
1281
- </div>
1282
- <canvas
1283
- style={canvas}
1284
- onMouseDown={this.startDrawing}
1285
- onMouseUp={this.endDrawing}
1286
- onMouseMove={this.draw}
1287
- ref={this.canvasRef}
1288
- height={canvasHeight}
1289
- width={canvasWidth}
1290
- />
1291
- <img
1292
- style={imgStyle}
1293
- src={dataUrl}
1294
- ref={this.imageRef}
1295
- />
1296
- </div>
1297
- </div>
1298
- );
1299
- }
1300
- }
1301
- ```
1302
-
1303
- <h3 id="rendering-application">Rendering your application</h3>
1304
-
1305
- - Select an existing HTML element (typically having <code>id</code> as "root" or "app") and use the <code>Anu.render()</code> method which takes two arguments:
1306
- - The first one is the component you want to add to the DOM tree.
1307
- - The second one is a valid HTML element already in the DOM tree, to which you want to attach your component.
1308
-
1309
- ```typescript
1310
- const ID = 'root';
1311
- Anu.render(
1312
- <App />,
1313
- document.getElementById(ID) as Element
1314
- );
1315
- ```
1316
-
1317
- <br>
1318
-
1319
- <h2 id="history-api">Routing - <strong>The History API</strong></h2>
1320
-
1321
- <h3 id="linking-and-routing">Linking and routing</h3>
1322
-
1323
- - You can use <code>&lt;Anu.History.Link /&gt;</code> elements those work just like "normal" links but without the reload of the page:
1324
- - The <code>&lt;Anu.History.Link /&gt;</code> element has a <code>to</code> prop. When clicking on it, its <code>to</code> prop will be matched against the <code>path</code> prop of <code>&lt;Anu.History.Route /&gt;</code>.
1325
- - Use the <code>&lt;Anu.History.Route /&gt;</code> component that takes a <code>path</code> argument and if the link you clicked matches the <code>path</code>, it will render the attached component:
1326
- - The <code>&lt;Anu.History.Route /&gt;</code> component has a <code>path</code> prop to match against the URL. If it has an <code>exact</code> prop, it doesn't allow partial matching.
1327
- - The <code>&lt;Anu.History.Route /&gt;</code> can also have a <code>component</code> (must be a component) or a <code>render</code> prop (it must be a function that returns a component).
1328
-
1329
- ```typescript
1330
- import Anu, { AnuElement, Props } from 'anu-verzum';
1331
-
1332
- interface RouteMatch { path: string | null; url: string; isExact: boolean; }
1333
-
1334
- const Home = (): AnuElement => <h2>Home</h2>;
1335
- const About = (): AnuElement => <h2>About</h2>;
1336
-
1337
- interface TopicProps extends Props { topicId: string; }
1338
- const Topic = ({ topicId }: TopicProps): AnuElement => <h3>{topicId}</h3>;
1339
-
1340
- interface TopicsProps extends Props { match: RouteMatch; }
1341
- const Topics = ({ match }: TopicsProps): AnuElement => {
1342
- const items: Array<{ name: string; slug: string }> = [
1343
- { name: 'Rendering with React', slug: 'rendering' },
1344
- { name: 'Components', slug: 'components' },
1345
- { name: 'Props v. State', slug: 'props-v-state' }
1346
- ];
1347
- return (
1348
- <div>
1349
- <h2>Topics</h2>
1350
- <ul>
1351
- {items.map(({ name, slug }) => (
1352
- <li key={slug}>
1353
- <Anu.History.Link to={`${match.url}/${slug}`}>{name}</Anu.History.Link>
1354
- </li>
1355
- ))}
1356
- </ul>
1357
- {items.map(({ name, slug }) => (
1358
- <Anu.History.Route key={slug} path={`${match.path ?? ''}/${slug}`} render={() => (
1359
- <Topic topicId={name} />
1360
- )} />
1361
- ))}
1362
- <Anu.History.Route exact path={match.url} render={() => (
1363
- <h3>Please select a topic.</h3>
1364
- )} />
1365
- </div>
1366
- );
1367
- };
1368
- // Usage in application:
1369
- const RouterApp = (): AnuElement => (
1370
- <div>
1371
- <ul>
1372
- <li><Anu.History.Link to="/">Home</Anu.History.Link></li>
1373
- <li><Anu.History.Link to="/about">About</Anu.History.Link></li>
1374
- <li><Anu.History.Link to="/topics">Topics</Anu.History.Link></li>
1375
- </ul>
1376
- <hr />
1377
- <Anu.History.Route exact path="/" component={Home} />
1378
- <Anu.History.Route path="/about" component={About} />
1379
- <Anu.History.Route path="/topics" component={Topics} />
1380
- </div>
1381
- );
1382
- ```
1383
-
1384
- <h3 id="redirecting-with-history-redirect">Redirecting - <strong>The <code>&lt;Anu.History.Redirect /&gt;</code> component</strong></h3>
1385
-
1386
- - If the user needs to be redirected (e.g. if not logged in), use the <code>&lt;Anu.History.Redirect /&gt;</code> component:
1387
- - The <code>&lt;Anu.History.Redirect /&gt;</code> takes a <code>to</code> (URL) and an optional <code>push</code> (boolean) argument.<br>
1388
- This will tell the System to render the component which can be found on the <code>to</code> URL (which is basically an <code>&lt;Anu.History.Route /&gt;</code> which will render the corresponding component).
1389
- If <code>push</code> is present, it will push the URL into the <strong>History API</strong> instead of replacing the current one.
1390
-
1391
- ```typescript
1392
- declare const loggedIn: boolean;
1393
-
1394
- const RouteredApp = (): AnuElement => (
1395
- // ...
1396
- <Anu.History.Route
1397
- path="/something"
1398
- render={() => {
1399
- if (loggedIn) {
1400
- return <MainPageComponent />;
1401
- } else {
1402
- return <Anu.History.Redirect to="/" push />;
1403
- }
1404
- }}
1405
- />
1406
- );
1407
- ```
1408
-
1409
- <h3 id="redirecting-with-history-goto">Redirecting from within the code - <strong>The <code>Anu.History.goTo()</code> method</strong></h3>
1410
-
1411
- - If you need to call a route from a function, use the <code>Anu.History.goTo()</code> function.
1412
- - It takes a <code>path</code> string argument.
1413
- If not specified, it will use its default path (<code>'/'</code>).
1414
- - Optionally, it can take a <code>replace</code> boolean argument.
1415
- If set to <code>true</code>, it will replace the current URL. By default, it pushes into the History API.
1416
- - Please only use <code>Anu.History.goTo()</code> if you need to redirect user inside the code based on functionality (like in <code>if</code> statement) for accessibility reasons.
1417
-
1418
- ```typescript
1419
- // Pushes URL into History API by default:
1420
- Anu.History.goTo('/about');
1421
- // Replaces current URL with the 'path' argument in History API:
1422
- Anu.History.goTo('/about', true);
1423
- ```
1424
-
1425
- <h3 id="reading-url-parameters">Reading URL parameters - <strong><code>Anu.History.getUrlParams()</code> and <code>Anu.History.getAllUrlParamNames()</code></strong></h3>
1426
-
1427
- URLs are expected to follow the REST resource-path convention:
1428
-
1429
- ```
1430
- /{noun-plural}/{id}/{noun-plural}/{id}/...
1431
- ```
1432
-
1433
- The router parses the current pathname into named parameters by singularizing each noun segment and appending `Id`.
1434
- A trailing action segment (odd-length path) is ignored.
1435
-
1436
- | URL | Extracted params |
1437
- |---|---|
1438
- | `/products` | `{}` |
1439
- | `/products/a3f8c2d1` | `{ productId: 'a3f8c2d1' }` |
1440
- | `/users/asdf1234/products` | `{ userId: 'asdf1234' }` |
1441
- | `/users/asdf1234/products/ghjk5678` | `{ userId: 'asdf1234', productId: 'ghjk5678' }` |
1442
- | `/users/asdf1234/products/ghjk5678/delete` | `{ userId: 'asdf1234', productId: 'ghjk5678' }` |
1443
-
1444
- - **`Anu.History.getUrlParams(key)`** — returns the value of the named URL parameter derived from the current pathname, or `null` if the key is not present.
1445
- - **`Anu.History.getAllUrlParamNames()`** — returns an array of all parameter key names extractable from the current pathname. Useful for development and debugging.
1446
-
1447
- ```typescript
1448
- // URL: /users/asdf1234/products/ghjk5678
1449
- Anu.History.getUrlParams('userId'); // → 'asdf1234'
1450
- Anu.History.getUrlParams('productId'); // → 'ghjk5678'
1451
- Anu.History.getUrlParams('orderId'); // → null
1452
-
1453
- Anu.History.getAllUrlParamNames(); // → ['userId', 'productId']
1454
- ```
1455
-
1456
- <br>
1457
-
1458
- <h2 id="server-api">Calling the server asynchronously - <strong>The Server API</strong></h2>
1459
-
1460
- The <code>Anu.ServerAPI</code> is basically built on top of <code>Promise</code> and currently has 5 methods <code>get()</code>, <code>post()</code>, <code>put()</code>, <code>delete()</code> and <code>file()</code>.
1461
-
1462
- The <code>.catch()</code> callback receives <code>{ status, response }</code> where <code>response</code> is always <code>null</code> on failure. <code>status</code> is the HTTP status code for server-side errors (e.g. <code>404</code>, <code>500</code>) and <code>0</code> for network-level failures such as no internet connection, DNS error, or a CORS rejection (i.e. no HTTP response was received at all).
1463
-
1464
- ```typescript
1465
- Anu
1466
- .ServerAPI
1467
- .get<MyItem>('/app/my-server-url/1234')
1468
- .then(({ response }) => {
1469
- // response is MyItem | null
1470
- })
1471
- .catch(({ status }) => {
1472
- if (status === 0) {
1473
- // Network-level failure — no response was received.
1474
- // Show a "check your connection" message to the user.
1475
- } else if (status === 401) {
1476
- // Server responded with 401 Unauthorized — redirect to login.
1477
- Anu.History.goTo('/login');
1478
- } else if (status === 404) {
1479
- // Server responded with 404 Not Found.
1480
- } else {
1481
- // Any other HTTP error (403, 500, etc.).
1482
- }
1483
- });
1484
- ```
1485
-
1486
- <h3 id="get-and-delete-methods">The <strong>GET</strong> and <strong>DELETE</strong> HTTP methods</h3>
1487
-
1488
- - The <code>Anu.ServerAPI.get()</code> and <code>Anu.ServerAPI.delete()</code> perform a <strong>GET</strong> or <strong>DELETE</strong> HTTP method respectively to get data from the server or delete a specific data from the server
1489
- (usually referenced by an <code>id</code> attribute in both cases but not necessarily when using <strong>GET</strong>).<br>
1490
- They are faster than the <strong>POST</strong> or <strong>PUT</strong> HTTP methods but lack the security as well.
1491
- - Can take a <code>url</code> (string)
1492
- - <strong>MUST ALWAYS START WITH "<code>/app</code>"!</strong>
1493
- - If it contains <strong>whitespace</strong> characters, those will be replaced with <code>_</code>.
1494
- - Can also take an optional <code>params</code> (object) argument
1495
- - Returns a <code>Promise</code> object.
1496
- You can send params within the URL <strong>AND/OR</strong> as URI query parameters, e.g.:
1497
-
1498
- ```typescript
1499
- interface MyItem {
1500
- id: string;
1501
- name: string;
1502
- }
1503
-
1504
- const passedValueForGet = '1234'; // Represents an ID...
1505
- const paramsForGet = { key: 'value', nextKey: 'nextValue' };
1506
- // In this case, the XHR URL will be `/app/my-server-url/1234?key=value&nextKey=nextValue`:
1507
- Anu
1508
- .ServerAPI
1509
- .get<MyItem>(`/app/my-server-url/${passedValueForGet}`, paramsForGet)
1510
- .then(({ response }) => { /* response is MyItem | null */ })
1511
- .catch(({ status }) => { /* status is number */ });
1512
-
1513
- const passedValueForDelete = '1234'; // Represents an ID...
1514
- // In this case, the XHR URL will be `/app/my-server-url/1234`:
1515
- Anu
1516
- .ServerAPI
1517
- .delete<void>(`/app/my-server-url/${passedValueForDelete}`)
1518
- .then(({ response }) => { /* ... */ })
1519
- .catch(({ status }) => { /* status is number */ });
1520
- ```
1521
-
1522
- <h3 id="post-and-put-methods">The <strong>POST</strong> and <strong>PUT</strong> HTTP methods</h3>
1523
-
1524
- - The <code>Anu.ServerAPI.post()</code> and <code>Anu.ServerAPI.put()</code> perform an <strong>POST</strong> or <strong>PUT</strong> HTTP method respectively to send large data to the server and optionally get other data back.
1525
- - It takes a <code>url</code> (string) and a <code>data</code> (object) argument and returns a <code>Promise</code> object:
1526
-
1527
- ```typescript
1528
- interface CreateItemRequest {
1529
- name: string;
1530
- }
1531
-
1532
- interface CreateItemResponse {
1533
- id: string;
1534
- name: string;
1535
- }
1536
-
1537
- const dataForPost: CreateItemRequest = { name: 'My Item' };
1538
- Anu
1539
- .ServerAPI
1540
- .post<CreateItemResponse>('/app/my-server-url/', dataForPost)
1541
- .then(({ response }) => { /* response is CreateItemResponse | null */ })
1542
- .catch(({ status }) => { /* status is number */ });
1543
-
1544
- const passedValue = '1234'; // Represents an ID...
1545
- const dataForPut: Partial<CreateItemRequest> = { name: 'Updated Name' };
1546
- Anu
1547
- .ServerAPI
1548
- .put<CreateItemResponse>(`/app/my-server-url/${passedValue}`, dataForPut)
1549
- .then(({ response }) => { /* response is CreateItemResponse | null */ })
1550
- .catch(({ status }) => { /* status is number */ });
1551
- ```
1552
-
1553
- <h3 id="file-method">The <strong>FILE</strong> HTTP method</h3>
1554
-
1555
- - The <code>Anu.ServerAPI.file()</code> performs a POST HTTP method and encodes one or more files to send them to the server.
1556
- - It takes an <code>url</code> (string), a <code>file</code> (one <code>File</code> object or an <strong>ARRAY</strong> of <code>File</code> objects) argument and an optional <code>data</code> (object) and returns a <code>Promise</code> object:
1557
-
1558
- ```typescript
1559
- interface UploadResponse {
1560
- fileUrl: string;
1561
- }
1562
-
1563
- const data: Record<string, string> = { category: 'documents' };
1564
- const file: File = /* ... */ new File([], 'example.pdf');
1565
- Anu
1566
- .ServerAPI
1567
- .file<UploadResponse>('/app/my-server-url/', file, data)
1568
- .then(({ response }) => { /* response is UploadResponse | null */ })
1569
- .catch(({ status }) => { /* status is number */ });
1570
- ```
1571
-
1572
- <br>
1573
-
1574
- <h2 id="anulytics-api">Tracking users - <strong>The Anulytics API</strong></h2>
1575
-
1576
- <strong><code>&lt;ANUVerzum /&gt;</code> JS Framework</strong> comes with its built-in analytics tool.<br>
1577
- Unlike the most analytics tools, <strong>Anulytics API</strong> is designed to send the collected informations only once: when the user leaves the page (either changing tab, navigating to a different domain or closing the page / browser).<br>
1578
- This way, no unnecessary XHR calls are made, which could otherwise negatively impact performance and user experience.
1579
- - It has two public interfaces: <code>&lt;Anu.Anulytics.Provider /&gt;</code> and <code>Anu.Anulytics.trackEvent()</code>.
1580
- - In order to use the built-in analytics tool, you must wrap all your components you want to track within <code>&lt;Anu.Anulytics.Provider /&gt;</code> component
1581
- (without this, you can't use <code>Anu.Anulytics.trackEvent()</code> event tracker functionality).
1582
-
1583
- <h3 id="anulytics-provider">Wrapping up your front-end project within the <code>&lt;Anu.Anulytics.Provider /&gt;</code> component</h3>
1584
-
1585
- - This component is responsible for the aggregation of the collected data and sends it to the server. It can have only one child element.
1586
- - Once the user leaves the page or navigates to / focuses on another tab, the component sends the collected data to the user-defined server via HTTP POST method.
1587
- - It takes 4 properties:
1588
- - <code>analyticsUrl</code>, which is a string that represents the URL of the server you want to send the data you collected.
1589
- - <code>userData</code> is an object of data about the user.
1590
- Make sure that you ask for permissions from the end-users and build the <code>userData</code> object accordingly in order to stay GDPR-compliant.
1591
- Always ask permission from the end-user, highlighting the types of data you wish to use.
1592
- - <code>onSuccess</code> callback is a custom callback function that will be fired when data transmission ended successfully.
1593
- - <code>onFail</code> callback is a custom callback function that will be fired when data transmission failed.
1594
-
1595
- ```typescript
1596
- declare const analyticsUrl: string;
1597
- declare const userData: Record<string, unknown>;
1598
- declare const handleSuccess: (response: unknown) => void;
1599
- declare const handleFail: (status: number) => void;
1600
-
1601
- const App = (): AnuElement => (
1602
- <Anu.Anulytics.Provider analyticsUrl={analyticsUrl} userData={userData} onSuccess={handleSuccess} onFail={handleFail}>
1603
- <Your_Site_Goes_Here />
1604
- </Anu.Anulytics.Provider>
1605
- );
1606
- ```
1607
-
1608
- - The <code>data</code> object sent to the server looks like the following:
1609
-
1610
- ```typescript
1611
- type Data = {
1612
- events: Event[];
1613
- startDate: Date;
1614
- endDate: Date;
1615
- system: {
1616
- referrer: string | null;
1617
- innerWidth: number;
1618
- isMobileAppInstalled: boolean;
1619
- userAgentData: {
1620
- userAgent: string | UserAgent[];
1621
- mobile: boolean;
1622
- platform: string | null;
1623
- };
1624
- };
1625
- user: Record<string, unknown>;
1626
- };
1627
- ```
1628
-
1629
- - The <code>Event</code> type is an object which <strong>key</strong> attribute is the <strong>type</strong> of the event (a string that represents the event, action type or a link if it was fired by navigation).<br>
1630
- These are the possible event keys <code>'initialization'</code>, <code>'navigation'</code>, <code>'userAction'</code>, <code>'stateChange'</code> or <code>'pageLeave'</code>:
1631
- - <code>'initialization'</code> is always set once the page loaded.
1632
- - <code>'navigation'</code> is set on inner navigation (if user clicks on a <code>&lt;Anu.History.Link /&gt;</code>, the <code>Anu.History.goTo()</code> was called or the user got redirected via <code>&lt;Anu.History.Redirect /&gt;</code>).
1633
- - <code>'userAction'</code> is set when <code>Anu.Anulytics.trackEvent()</code> has been used.<br>
1634
- This is the only public API of <strong>Anulytics API</strong>, typically used for tracking user events (events triggered by user interactions).
1635
- - <code>'stateChange'</code> is automatically called when an <strong>action</strong> gets dispatched.
1636
- - <code>'pageLeave'</code> is always set once the user focuses on another tab or closes the application (i.e.: leaving the current / actual page).
1637
-
1638
- - The value of the Event object contains three properties: <code>eventType</code>, <code>timestamp</code> and <code>properties</code>:
1639
- - The <code>eventType</code> is a string that represents the event fired (e.g.: a <strong>user event</strong> (mouse event, keyboard event, etc.), a <strong>URI</strong> where the user navigated or an <strong>action type</strong> the user dispatched).
1640
- - Note that the <code>eventType</code> property of <code>'initialization'</code>, <code>'navigation'</code> and <code>'pageLeave'</code> will always be a <strong>URI</strong>!
1641
- - The <code>timestamp</code> is always set: it is the POSIX timestamp of the fired event.
1642
- - The <code>properties</code> can either be an empty object (typically when the event was fired by navigation) or a <code>Property</code> type.
1643
- The <code>properties</code> are typically set when <code>Anu.Anulytics.trackEvent()</code> has been used or an action was dispatched.<br>
1644
- The <code>properties</code> are empty on <code>'initialization'</code>, <code>'navigation'</code> and <code>'pageLeave'</code>.
1645
-
1646
- ```typescript
1647
- type EventKey = 'initialization' | 'navigation' | 'userAction' | 'stateChange' | 'pageLeave';
1648
- type Event = {
1649
- [K in EventKey]?: {
1650
- eventType: string;
1651
- timestamp: Date;
1652
- properties: UserActionProperties | StateChangeProperties | Record<string, never>;
1653
- };
1654
- };
1655
- ```
1656
-
1657
- - The <code>UserActionProperties</code> type can have various properties, like <code>id</code>, <code>name</code>, <code>url</code> and <code>value</code>:
1658
- - The <code>id</code> is the value of the ID attribute of the DOM element with the given event handler tracked: <code>event.target.id</code> (optional but <strong>HIGHLY RECOMMENDED</strong> to set).
1659
- - The <code>name</code> is the value of the name attribute of the DOM element with the given event handler tracked: <code>event.target.name</code> (optional but <strong>HIGHLY RECOMMENDED</strong> to set).
1660
- - The <code>nodeName</code> is the name of the DOM node with the given event handler tracked: <code>event.target.nodeName</code>.
1661
- - The <code>keyCode</code> is the ASCII code of the key pressed (if the given element is a keyboard event, like <code>keyup</code>) or null.
1662
- - The <code>value</code> is the value of the DOM element with the given event handler tracked: <code>event.target.value</code>.
1663
- - The <code>pageX</code> is the X-coordinate of the mouse position from the left side of the page: <code>event.pageX</code>.
1664
- - The <code>pageY</code> is the Y-coordinate of the mouse position from the top of the page: <code>event.pageY</code>.
1665
- - The <code>scrollTop</code> is the amount the user scrolled from the top of the page.
1666
- - The <code>scrollLeft</code> is the amount the user scrolled from the left side of the page.
1667
- - The <code>url</code> is the URL of the page that contains the DOM element with the given event handler tracked.
1668
-
1669
- ```typescript
1670
- type UserActionProperties = {
1671
- id: string;
1672
- name: string;
1673
- nodeName: string;
1674
- keyCode: number | null;
1675
- value?: string;
1676
- pageX: number;
1677
- pageY: number;
1678
- scrollTop: number;
1679
- scrollLeft: number;
1680
- url: string;
1681
- props: Record<string, unknown> | null; // User-defined props passed to Anu.Anulytics.trackEvent()
1682
- };
1683
- ```
1684
-
1685
- - The <code>StateChangeProperties</code> type can have four properties: <code>url</code>, <code>prevState</code>, <code>action</code> and <code>nextState</code>:
1686
- - The <code>url</code> is the URL of the page that contains the DOM element with the given event handler tracked.
1687
- - The <code>prevState</code> is the global state object before the action was performed.
1688
- - The <code>action</code> is the dispatched action.
1689
- - The <code>nextState</code> is the global state object after the action was performed.
1690
-
1691
- ```typescript
1692
- type StateChangeProperties = {
1693
- url: string;
1694
- prevState: Record<string, unknown>;
1695
- action: Record<string, unknown>;
1696
- nextState: Record<string, unknown>;
1697
- };
1698
- ```
1699
-
1700
- - The <code>User</code> type is an object of the developer choice.
1701
- If not provided, an empty object will be passed.
1702
-
1703
- <h3 id="anulytics-track-event">Tracking events on elements using <code>Anu.Anulytics.trackEvent(event, props)</code></h3>
1704
-
1705
- - <strong>Anulytics API</strong> tracks visited links within the application out of the box.
1706
- If you want to track additional elements, call <code>Anu.Anulytics.trackEvent(event, props)</code> method inside the event handler.
1707
- - It takes two arguments:
1708
- - The <code>event</code> object is always needed because this object contains required information to track.
1709
- - The <code>props</code> is optional: either an object (not an array) or null.
1710
-
1711
- <br>
1712
-
1713
- <h2 id="context-api">Creating and accessing the context out of the "normal props flow" - <strong>The Context API</strong></h2>
1714
-
1715
- <h3 id="creating-context">Creating the context</h3>
1716
-
1717
- - To create a context provider and context consumer elements with default context, call the <code>Anu.createContext()</code>.
1718
- It takes a <code>context</code> argument which can be reached later as <code>context.defaultContext.value</code> and returns a context provider and consumer:
1719
-
1720
- ```typescript
1721
- interface ThemeContextValue {
1722
- theme: string;
1723
- }
1724
-
1725
- // This can be accessed as "context.defaultContext.value.theme":
1726
- const ThemedContext = Anu.createContext<ThemeContextValue>({ theme: 'Theme-1' });
1727
- ```
1728
-
1729
- <h3 id="usage-of-context-provider-and-consumers">Usage of the context provider and its consumer(s)</h3>
1730
-
1731
- - Context props defined on <code>&lt;ThemedContext.Provider /&gt;</code> can be accessed from within the function child of the <code>&lt;ThemedContext.Consumer /&gt;</code> as <code>context.value</code>.
1732
- - Context providers can have multiple context consumer descendents.
1733
- - Context consumers can have one function-as-a-child (which takes the <code>context</code> as argument) which must return a valid HTML, inline-SVG element, component (either class-based or function) or <code>null</code>.
1734
- - You can have as many elements between the context provider and consumer(s), as you want.<br>
1735
- No need to pass the <code>context</code> all the way down within the "props flow"; the function child of the context consumer will have access to it by default.<br>
1736
- It allows you to create your "intermediate" components without depending from the <code>context</code> (they don't need to be aware of it if they have nothing to do with it...).
1737
- - In TypeScript projects use <code>Provider</code> / <code>Consumer</code> — the callback argument type is inferred from the `createContext<T>()` type parameter, so no manual annotation is needed.
1738
- - **Avoid** <code>ContextProvider</code> / <code>ContextConsumer</code> in TypeScript projects. These aliases exist for internal use (e.g. by `Intl` and `Feature` modules) but carry looser types: the consumer callback argument is typed as `Props` instead of `ContextValue<T>`, so you lose type inference on `context.value` and `context.defaultContext`.
1739
-
1740
- ```typescript
1741
- import Anu, { AnuElement } from 'anu-verzum';
1742
-
1743
- const ComponentWithContext = (): AnuElement => {
1744
- const theme2 = 'Theme-2';
1745
- return (
1746
- <ThemedContext.Provider theme={theme2}>
1747
- <MyComponent1>
1748
- <MyComponentN>
1749
- <ThemedContext.Consumer>
1750
- {({ value: { theme }, defaultContext: { value: { theme: defaultTheme } } }) => (
1751
- // context type is inferred as ContextValue<ThemeContextValue>
1752
- // theme → "Theme-2", defaultTheme → "Theme-1"
1753
- <Anu.Fragment>
1754
- <span>{theme}</span>
1755
- <span>{defaultTheme}</span>
1756
- </Anu.Fragment>
1757
- )}
1758
- </ThemedContext.Consumer>
1759
- </MyComponentN>
1760
- </MyComponent1>
1761
- </ThemedContext.Provider>
1762
- );
1763
- };
1764
- ```
1765
-
1766
- <br>
1767
-
1768
- <strong>The next APIs (<code>Anu.Connector.connect()</code> and <code>&lt;Anu.Connector.Provider /&gt;</code>; <code>&lt;Anu.Intl.Provider /&gt;</code>, <code>&lt;Anu.Intl.FormattedMessage /&gt;</code>, <code>Anu.Intl.formatMessage()</code> and <code>Anu.Intl.abbreviateNumber()</code>; <code>&lt;Anu.Feature.Provider /&gt;</code> and <code>&lt;Anu.Feature.Toggle /&gt;</code>) are strongly relying on the Context API</strong><br>
1769
-
1770
- <br>
1771
-
1772
- <h2 id="store-api">Storing and mutating the global state on actions dispatched, memoizing complex conversions on the global state and combining reducers - <strong>The Store API</strong></h2>
1773
-
1774
- <h3 id="dispatching-actions">Dispatching actions</h3>
1775
-
1776
- - Create action creator functions (returns an object) or asynchronous action creator functions (returns a function) to dispatch action(s):
1777
- - To create a synchronous action creator, simply create a function that returns an object.
1778
- You <strong>MUST</strong> specify its <code>type</code> member - the reducer will react when an action with this type was dispatched.
1779
- - If you create asynchronous action creator as well, then remember, the action creator should return a <strong>FUNCTION</strong> instead of an object (in this case, you <strong>MUST</strong> use a middleware - see <a href="#creating-the-store">Creating the store</a> section):
1780
- - The outermost function will take whatever we pass to it
1781
- - That returned function takes 2 arguments: <code>dispatch</code> and <code>getState</code>
1782
- - Here. you can do additional things (like getting out a value from the state using <code>getState()</code> or dispatching an action using <code>dispatch()</code>) before returning the final payload (most likely an AJAX request).
1783
-
1784
- ```typescript
1785
- import Anu, { Action, ThunkAction, Dispatch } from 'anu-verzum';
1786
-
1787
- // Simple action creator:
1788
- const myActionCreator = (passedProps: unknown): Action => ({
1789
- type: 'MY_ACTION', // Must have!
1790
- payload: { passedProps }
1791
- });
1792
- // Async action creator - requires middleware:
1793
- const myAsyncActionCreator = (value: string): ThunkAction => (dispatch, getState) => {
1794
- // call a function that dispatches action(s) or returning a promise...
1795
- };
1796
- ```
1797
-
1798
- <strong>Example</strong> - <i>creating a "simple" action creator</i>:
1799
-
1800
- ```typescript
1801
- // Action type constants:
1802
- const mySimpleActionTypes = {
1803
- ACTION: 'MY_SIMPLE_ACTION' as const
1804
- };
1805
-
1806
- // Typed action interface:
1807
- interface MySimpleAction extends Action {
1808
- type: typeof mySimpleActionTypes.ACTION;
1809
- payload: { param: string };
1810
- }
1811
-
1812
- // Action creator:
1813
- const mySimpleActionCreator = (param: string): MySimpleAction => ({
1814
- type: mySimpleActionTypes.ACTION,
1815
- payload: { param }
1816
- });
1817
- ```
1818
-
1819
- <strong>Example</strong> - <i>creating an "asynchronous" action creator</i>:
1820
-
1821
- ```typescript
1822
- // Action type constants:
1823
- const myAsyncActionTypes = {
1824
- PENDING: 'MY_PENDING_ACTION' as const,
1825
- FULFILLED: 'MY_FULFILLED_ACTION' as const,
1826
- REJECTED: 'MY_REJECTED_ACTION' as const
1827
- };
1828
-
1829
- // Typed action interfaces:
1830
- interface PendingAction extends Action { type: typeof myAsyncActionTypes.PENDING; }
1831
- interface FulfilledAction extends Action { type: typeof myAsyncActionTypes.FULFILLED; payload: { response: unknown }; }
1832
- interface RejectedAction extends Action { type: typeof myAsyncActionTypes.REJECTED; payload: { status: number }; }
1833
- type MyAsyncAction = PendingAction | FulfilledAction | RejectedAction;
1834
-
1835
- // Async action creator:
1836
- const myAsyncActionCreator = (value: string): ThunkAction => dispatch => {
1837
- dispatch({ type: myAsyncActionTypes.PENDING });
1838
- return Anu
1839
- .ServerAPI
1840
- .get(`/app/my/server/url/${value}`)
1841
- .then(({ response }) => dispatch({ type: myAsyncActionTypes.FULFILLED, payload: { response } }))
1842
- .catch(({ status }) => dispatch({ type: myAsyncActionTypes.REJECTED, payload: { status } }));
1843
- };
1844
- ```
1845
-
1846
- <h3 id="handling-actions-with-reducers">Handling actions with reducers</h3>
1847
-
1848
- - Note that if it is added to the rootReducer, it will represent that "sub-tree" of the state so,
1849
- it should return that part, with the updated desired values:
1850
- - Note that the relationship between actions and reducers is M:N!
1851
- - <strong>NEVER</strong> mutater the state directly!
1852
-
1853
- ```typescript
1854
- import Anu, { Reducer } from 'anu-verzum';
1855
-
1856
- interface MyReducerState {
1857
- statePart: { part1: string };
1858
- }
1859
-
1860
- // Initial state:
1861
- const initialStateForMyReducer1: MyReducerState = { statePart: { part1: 'defaultValue' } };
1862
- // Reducer:
1863
- const myReducer1: Reducer<MyReducerState> = (state = initialStateForMyReducer1, action) => {
1864
- switch (action.type) {
1865
- case 'ACTION_1': {
1866
- return { statePart: { part1: action.payload.part1 } };
1867
- }
1868
- // ...
1869
- default: {
1870
- return state;
1871
- }
1872
- }
1873
- };
1874
- ```
1875
-
1876
- <strong>Example</strong> - <i>handling the previous actions</i>:
1877
-
1878
- ```typescript
1879
- type LoadingStatus = 'PENDING' | 'FULFILLED' | 'REJECTED' | null;
1880
-
1881
- interface MyFeatureState {
1882
- param: string;
1883
- status: LoadingStatus;
1884
- data: unknown;
1885
- errorCode: number | null;
1886
- }
1887
-
1888
- const defaultState: MyFeatureState = { param: '', status: null, data: null, errorCode: null };
1889
-
1890
- const myReducerForSimpleActionCreator: Reducer<MyFeatureState> = (state = defaultState, { type, payload }) => {
1891
- switch (type) {
1892
- case mySimpleActionTypes.ACTION: {
1893
- const { param } = payload as { param: string };
1894
- return { ...state, param };
1895
- }
1896
- case myAsyncActionTypes.PENDING: {
1897
- return { ...state, status: 'PENDING' };
1898
- }
1899
- case myAsyncActionTypes.FULFILLED: {
1900
- const { response } = payload as { response: unknown };
1901
- return { ...state, status: 'FULFILLED', data: response, errorCode: null };
1902
- }
1903
- case myAsyncActionTypes.REJECTED: {
1904
- const { status } = payload as { status: number };
1905
- return { ...state, status: 'REJECTED', data: null, errorCode: status };
1906
- }
1907
- default: {
1908
- return state;
1909
- }
1910
- }
1911
- };
1912
- ```
1913
-
1914
- <h3 id="memoizing-state-conversions">Memoizing (global) state conversions</h3>
1915
-
1916
- - Selectors are memoized functions which come handy if you need to do expensive calculations or conversions on the global state.
1917
- - To create selector, use the <code>Anu.store.createSelector()</code> (it comes handy in <code>mapStateToProps()</code> - see <a href="#connector-api">Connecting components to the global state - The Connector API</a> section):
1918
- - Its first argument is either a single "getter" function or an array of up to four "getter" functions, each returning a desired slice of the global state object.
1919
- These functions must always return something.
1920
- - The second argument is a "handler" function which takes as many arguments as "getter" functions you defined; these arguments are the return values of the "getter" functions.
1921
- Their number and order is the same as of the "getters" within the first argument of the <code>Anu.store.createSelector()</code>.
1922
- - In TypeScript, when you pass the getter array inline, the handler's parameter types are inferred automatically — no manual annotations needed.
1923
-
1924
- ```typescript
1925
- import Anu, { SelectorFn } from 'anu-verzum';
1926
-
1927
- interface MyGlobalState {
1928
- myStatePart1: string;
1929
- myStatePart2: number[];
1930
- }
1931
-
1932
- // Getter functions:
1933
- const getStatePart1: SelectorFn<MyGlobalState, string> = state => state.myStatePart1;
1934
- const getStatePart2: SelectorFn<MyGlobalState, number[]> = state => state.myStatePart2;
1935
-
1936
- // Single getter — transformation receives the getter's return type directly:
1937
- const mySimpleSelector = Anu.store.createSelector(
1938
- getStatePart1,
1939
- (part1) => part1.toUpperCase() // part1 inferred as string
1940
- );
1941
-
1942
- // Multiple getters — pass the array inline so TypeScript can infer a tuple:
1943
- const mySelector = Anu.store.createSelector(
1944
- [getStatePart1, getStatePart2],
1945
- (part1, part2) => `${part1}: ${part2.join(', ')}`
1946
- // part1 inferred as string, part2 as number[]
1947
- );
1948
- ```
1949
-
1950
- <h3 id="combining-reducers">Combining reducers</h3>
1951
-
1952
- - You can combine multiple reducers using the <code>Anu.store.combineReducers()</code> if you need a more complex state shape:
1953
- - The function receives one (object) argument - the "key" of the item will be the name of the corresponding state-part, the "value" is the reducer you want to add to the combined reducer.
1954
- - <code>Anu.store.combineReducers()</code> can be used multiple times in the application.
1955
-
1956
- ```typescript
1957
- import Anu, { Reducer } from 'anu-verzum';
1958
-
1959
- interface GlobalState {
1960
- myStatePart1: MyReducerState;
1961
- myStatePart2: MyFeatureState;
1962
- }
1963
-
1964
- const combinedReducer: Reducer<GlobalState> = Anu.store.combineReducers<GlobalState>({
1965
- myStatePart1: myReducer1,
1966
- myStatePart2: myReducerForSimpleActionCreator
1967
- });
1968
- ```
1969
-
1970
- - It also makes sense to create an initial state description (where you can reuse the initial states related to the reducers you just combined).
1971
- - The "key" of this object must match with the corresponding "key" you used within the combined reducer, the "value" is the initial state (the one you (possibly) used for the "single" reducer).
1972
- It is not mandatory however, it comes handy when your state object becomes large and complex:
1973
-
1974
- ```typescript
1975
- const initialState: GlobalState = {
1976
- myStatePart1: initialStateForMyReducer1,
1977
- myStatePart2: defaultState
1978
- };
1979
- ```
1980
-
1981
- <h3 id="creating-the-store">Creating the store</h3>
1982
-
1983
- - The store object stores the global state object (that can be reached using the <code>store.getState()</code> method) and it also has the <code>store.dispatch()</code>, <code>store.subscribe()</code> and <code>store.unsubscribe()</code> methods.
1984
- You will likely use the <code>store.getState()</code> and <code>store.dispatch()</code> methods only because subscribing and unsubscribing functionalities are "wired" into the <code>&lt;Anu.Connector.Provider /&gt;</code> and the "connected"
1985
- (also known as "container") component(s) created by using the <code>Anu.Connector.connect()</code> - see <a href="#connector-api">Connecting components to the global state - The Connector API</a> section.
1986
- - Create a store object using <code>Anu.store.createStore()</code>:
1987
- - The first argument is the <code>rootReducer</code> which is always needed (can be either a "single" reducer or a combination of ("single" and/or combined) reducers),
1988
- - The second <code>initialState</code> argument is required. Pass the initial state object for your store here.
1989
- This argument is useful if you want to have initialized values for your application before dispatching your first action.
1990
- - The third argument is optional, you can use it if you want to apply middleware functionalities like dispatching asynchronous actions (e.g. AJAX calls or delayed calls).
1991
- In this case, the built-in <code>Anu.store.middleware.applyMiddleware()</code> can be passed:
1992
- - It can take any numbers of callbacks (even zero) those will run on every actions dispatched.
1993
- - There are 2 built-in callbacks you can use out-of-the-box:
1994
- - The <code>Anu.store.middleware.loggingMiddleware()</code> is a logger; it comes handy when in development mode,
1995
- - The <code>Anu.store.middleware.thunkMiddleware()</code> enables you to use asynchronous action creators (those returning functions instead of objects) as well.
1996
-
1997
- ```typescript
1998
- import Anu, { Store } from 'anu-verzum';
1999
-
2000
- // Assuming that the 'rootReducer' is the 'combinedReducer' created before:
2001
- const rootReducer = combinedReducer;
2002
- // If you don't use middleware (i.e. you don't need to handle asynchronous calls):
2003
- const syncStore: Store<GlobalState> = Anu.store.createStore<GlobalState>(rootReducer, initialState);
2004
- // Otherwise, if you wish handle asynchronous calls, like AJAX requests:
2005
- const asyncStore: Store<GlobalState> = Anu.store.createStore<GlobalState>(
2006
- rootReducer,
2007
- initialState,
2008
- Anu.store.middleware.applyMiddleware(
2009
- Anu.store.middleware.thunkMiddleware,
2010
- Anu.store.middleware.loggingMiddleware
2011
- )
2012
- );
2013
- ```
2014
-
2015
- <br>
2016
-
2017
- <h2 id="connector-api">Connecting components to the global state - <strong>The Connector API</strong></h2>
2018
-
2019
- This API is designed to connect your global store with elements within the presentation layer.
2020
-
2021
- <h3 id="connect-to-store">Connect to the store</h3>
2022
-
2023
- - Wrap the outermost component (this should contain all your "container" components - see <a href="#create-container-component">Create container component</a> section) within <code>&lt;Anu.Connector.Provider /&gt;</code> and pass the store object:
2024
-
2025
- ```typescript
2026
- import Anu, { Component, AnuElement } from 'anu-verzum';
2027
-
2028
- class App extends Component {
2029
- render(): AnuElement {
2030
- return (
2031
- <Anu.Connector.Provider store={store}>
2032
- <Your_Page />
2033
- </Anu.Connector.Provider>
2034
- );
2035
- }
2036
- }
2037
- ```
2038
-
2039
- <h3 id="create-container-component">Create container component</h3>
2040
-
2041
- - To connect your "container" component to the global store, simply define a component (either a function or class-based) you want to connect.
2042
- It is a "curried function" (a function returns a function, also known as "high-order function" (HOF)).
2043
- The first function takes two arguments: the <code>mapStateToProps()</code> and <code>mapDispatchToProps()</code>, the second one takes the wrapped component.
2044
- - Be aware that the props of wrapped component are the combination of the props passed to the (outer) component (i.e. when rendering in the return statement) and those returned by <code>mapStateToProps()</code> and <code>mapDispatchToProps()</code>!
2045
- - Define maximum two functions (they are typically called <code>mapStateToProps()</code> and <code>mapDispatchToProps()</code>; if either one is not defined, pass <code>null</code> respectively):
2046
- - The <code>mapStateToProps()</code> can fetch values from the global state and injects them as props into the wrapped component you want to connect:
2047
- - It takes the <code>state</code> as the first argument and an optional <code>ownProps</code> which is basically the object of the props you pass down from the caller component as part of the "props flow".
2048
- - The function must return an object of props which can now be used by the wrapped component as if they were "regular" props.
2049
- - This function is the ideal place to use selectors - see <a href="#memoizing-state-conversions">Memoizing (global) state conversions</a> section.
2050
- - The <code>mapDispatchToProps()</code> can define functions those dispatch actions and injects them as props into the wrapped component you want to connect:
2051
- - It takes the <code>dispatch</code> as the first argument and an optional <code>ownProps</code> which is basically the object of the props you pass down from the caller component as part of the "props flow".
2052
- - The function must return an object of props (functions-as-props those dispatch actions - call action creators) which can now be used by the wrapped component as if they were "regular" props.
2053
-
2054
- ```typescript
2055
- import Anu, { Props, AnuElement, Component, Dispatch } from 'anu-verzum';
2056
-
2057
- // Own props passed from the parent component:
2058
- interface OwnProps extends Props {
2059
- id: string;
2060
- }
2061
-
2062
- // Props injected from the global state:
2063
- interface StateProps {
2064
- propName1: string;
2065
- propNameN: string;
2066
- }
2067
-
2068
- // Props injected as dispatchers:
2069
- interface DispatchProps {
2070
- dispatcherPropName1: (passedProps: Record<string, unknown>) => void;
2071
- }
2072
-
2073
- type AllProps = OwnProps & StateProps & DispatchProps;
2074
-
2075
- // Create function component:
2076
- const WrappedComponent = (props: AllProps): AnuElement => (
2077
- <Component_to_render />
2078
- );
2079
- // Or class component:
2080
- class WrappedComponent extends Component<AllProps> {
2081
- render(): AnuElement {
2082
- return <Component_to_render />;
2083
- }
2084
- }
2085
-
2086
- const mapStateToProps = (state: GlobalState, ownProps: OwnProps): StateProps => {
2087
- const { myStatePart1 } = state;
2088
- return {
2089
- propName1: myStatePart1.statePart.part1,
2090
- // Using selector to derive some parts of the global state - see 'Store API' section:
2091
- propNameN: mySelector(state)
2092
- };
2093
- };
2094
-
2095
- const mapDispatchToProps = (dispatch: Dispatch, ownProps: OwnProps): DispatchProps => ({
2096
- dispatcherPropName1: (passedProps) => dispatch(mySimpleActionCreator(passedProps.param as string))
2097
- });
2098
-
2099
- // Connect:
2100
- const ContainerComponent = Anu.Connector.connect(mapStateToProps, mapDispatchToProps)(WrappedComponent);
2101
- ```
2102
-
2103
- <br>
2104
-
2105
- <h2 id="intl-api">Supporting multiple languages - <strong>The Intl API</strong></h2>
2106
-
2107
- The basic concept behind the <strong>Intl API</strong> is that you define different JSON objects for each languages you support in your application and
2108
- you refer to the text-part you need.
2109
- - Use the <code>&lt;Anu.Intl.Provider /&gt;</code> to set the supported language files for your "static texts" (no end-user-entered texts will be translated...) and the set the selected language in your app.
2110
- - Then, use the <code>&lt;Anu.Intl.FormattedMessage /&gt;</code> to place the text on the selected language referred by the <code>id</code> property.
2111
- You can also use <code>Anu.Intl.formatMessage()</code> when you want to translate a property value only (e.g. the placeholder text in an input element).
2112
- - If the text itself should also contain a dynamic text value, you can pass an optional <code>values</code> property which contains key-value pairs.
2113
- - In the text inside the language file, you should refer for the key using the <code>{key}</code> format.
2114
-
2115
- <h3 id="creating-language-objects">Creating the supported language objects</h3>
2116
-
2117
- - These are basically simple objects with key-value pairs.
2118
- The key is always the ID of the referred text (see <a href="#formatting-component-texts">Formatting component texts</a> and <a href="#formatting-attribute-texts">Formatting attribute texts</a> section), the value is the value to print.
2119
- - Create as many language objects as many languages you want to support.
2120
- Don't forget that the IDs should match in each objects because they will be the key used by the <strong>Intl API</strong> to find the text for the selected language.
2121
- - Wrap these objects within one main object.
2122
- This time, the keys will be the strings you mark the supported languages and the corresponding values will be the objects created before.
2123
-
2124
- ```typescript
2125
- type MessageMap = Record<string, string>;
2126
-
2127
- const messages_hu: MessageMap = {
2128
- 'app.title': 'Üdvözöllek!',
2129
- 'app.description': 'Örülök, hogy újra itt vagy, {name}!',
2130
- 'app.search.placeholder1': 'Ide írd be a keresett kulcsszót!',
2131
- 'app.search.placeholder2': 'Kedves {name}, ide írd be a keresett kulcsszót!'
2132
- };
2133
- const messages_en: MessageMap = {
2134
- 'app.title': 'Welcome!',
2135
- 'app.description': 'I\'m glad that you are here again, {name}!', // 'name' is a placeholder
2136
- 'app.search.placeholder1': 'Type search keyword here!',
2137
- 'app.search.placeholder2': 'Dear {name}, type search keyword here!'
2138
- };
2139
- const messages: Record<string, MessageMap> = {
2140
- 'hu': messages_hu,
2141
- 'en': messages_en
2142
- };
2143
- ```
2144
-
2145
- <h3 id="adding-language-objects-to-provider">Adding the language objects to the Intl provider</h3>
2146
-
2147
- - Within this step, wrap the outermost component (this should contain all your "internationalized" components, texts, etc.) within the <code>&lt;Anu.Intl.Provider /&gt;</code>.
2148
- This component takes three props:
2149
- - The <code>messages</code> property should be the object that contains all the translations for all the languages your application supports.
2150
- - The <code>locale</code> property is practically a short string that is the preferred language (it must match with one of the keys of the outermost object passed as <code>messages</code>).
2151
- - The <code>defaultLocale</code> property is optional and will refer to the default language if <code>messages[locale]</code> couldn't be found.
2152
-
2153
- ```typescript
2154
- // This will use the first 2 letters of the language set in browser settings (e.g.: "en", "it", "hu", ...):
2155
- const locale: string = navigator.language.split(/[-_]/)[0];
2156
- const messages: Record<string, MessageMap> = { /* ... */ };
2157
- const LocaleContainer = (): AnuElement => (
2158
- <Anu.Intl.Provider messages={messages} locale={locale} defaultLocale="hu">
2159
- <Your_page />
2160
- </Anu.Intl.Provider>
2161
- );
2162
- ```
2163
-
2164
- <h3 id="formatting-component-texts">Formatting component texts</h3>
2165
-
2166
- - Use <code>&lt;Anu.Intl.FormattedMessage /&gt;</code> for the specific text (referred by <code>id</code> property) - if used as a rendered element:
2167
- - The first, required argument is the <code>id</code> property which is the key of the text in the user-defined language objects.
2168
- - It can take an optional <code>values</code> property which is an object:
2169
- - The key property must match the placeholder you want to replace with your dynamic value within the text inside your language object you refer (i.e. wrapped between <code>{</code> and <code>}</code>).
2170
- - The value is what should be used to replace the placeholder.
2171
-
2172
- ```typescript
2173
- const ComponentWithFormattedMessages = (): AnuElement => {
2174
- const name = 'Anubis';
2175
- return (
2176
- <Anu.Fragment>
2177
- <h1>
2178
- <Anu.Intl.FormattedMessage id="app.title" defaultMessage="Welcome" />
2179
- </h1>
2180
- <p>
2181
- <Anu.Intl.FormattedMessage id="app.description" values={{ name }} defaultMessage="Welcome, User!" />
2182
- </p>
2183
- </Anu.Fragment>
2184
- );
2185
- };
2186
- ```
2187
-
2188
- <h3 id="formatting-attribute-texts">Formatting attribute texts</h3>
2189
-
2190
- - Use <code>Anu.Intl.formatMessage()</code> for the specific text (referred by <code>id</code> property) - if used as an attribute of an element:
2191
- - The first, required argument is the <code>id</code> property which is the key of the text in the user-defined JSON files (e.g.: <code>messages_hu[id]</code> or <code>messages_en[id]</code>).
2192
- - The second argument is the <code>values</code> property which is either an object or <code>null</code>. If defined:
2193
- - The key property must match the placeholder you want to replace with your dynamic value within the text inside your language object you refer (i.e. wrapped between <code>{</code> and <code>}</code>).
2194
- - The value is what should be used to replace the placeholder.
2195
- - It can also take an optional <code>defaultMessage</code> property which is used if the searched text couldn't been found.
2196
- - If the ID can not be found and no <code>defaultMessage</code> is passed, the function will return the <code>id</code> value as a string.
2197
-
2198
- ```typescript
2199
- const ComponentWithFormatMessageFunctionCall = (): AnuElement[] => [
2200
- <input placeholder={Anu.Intl.formatMessage('app.search.placeholder1', undefined, 'Search...')} />,
2201
- <input placeholder={Anu.Intl.formatMessage('app.search.placeholder2', { name: 'Anubis' }, 'Search...')} />
2202
- ];
2203
- ```
2204
-
2205
- <h3 id="abbreviating-numbers">Abbreviating numbers</h3>
2206
-
2207
- - If you want to abbreviate a large number, the <code>Anu.Intl.abbreviateNumber()</code> function comes handy.
2208
- - This function is also part of the <strong>INTL API</strong> so, it is able to read the language set within <code>&lt;Anu.IntlProvider /&gt;</code> (and you must use it within <code>&lt;Anu.IntlProvider /&gt;</code>).<br>
2209
- This currently supports only the <i>Hungarian</i> and the <i>English</i> abbreviations by default, but you can define your custom abbreviation rules as well.
2210
- - The function takes two arguments:
2211
- - The first argument is the numeric <code>value</code>: the number to abbreviate.<br>
2212
- If there is no match for the selected language and you didn't specify a custom <code>options</code> object, the system will fall back to the default (English) options.
2213
- - The second, optional argument is the <code>options</code>, which is an object:
2214
- - The first member is <code>units</code> - an array of strings to be used as abbreviation units.<br>
2215
- When not specified, it will use the default (english) abbreviation units ('K', 'M', 'B', 'T').
2216
- - The second member is <code>decimalPlaces</code> - a number that represents the decimal places.<br>
2217
- If not specified, the system will fall back to use two decimal places.
2218
- - The third member is <code>decimalSign</code> - a string to replace the standard (at least in english speaking countries) dot (<code>.</code>) sign with the one of choice.<br>
2219
- If not specified, the system will fall back to the default dot (<code>.</code>) sign.
2220
-
2221
- ```typescript
2222
- import Anu, { AbbreviateNumberOptions } from 'anu-verzum';
2223
-
2224
- declare const value: number;
2225
- declare const option: AbbreviateNumberOptions;
2226
-
2227
- // Usage with default options:
2228
- Anu.Intl.abbreviateNumber(value);
2229
- // Usage with custom options:
2230
- Anu.Intl.abbreviateNumber(value, option);
2231
- ```
2232
-
2233
- <strong>Example</strong> - <i>abbreviating a number using default (built-in) options</i>:
2234
-
2235
- ```typescript
2236
- Anu.Intl.abbreviateNumber(10000000000); // → "10B"
2237
- Anu.Intl.abbreviateNumber(100000000000); // → "100B"
2238
- Anu.Intl.abbreviateNumber(1000000000000); // → "1T"
2239
- Anu.Intl.abbreviateNumber(-10000000); // → "-10M"
2240
- Anu.Intl.abbreviateNumber(-10000); // → "-10K"
2241
- Anu.Intl.abbreviateNumber(-10234); // → "-10.23K"
2242
- ```
2243
-
2244
- <strong>Example</strong> - <i>abbreviating with custom options</i>:
2245
-
2246
- ```typescript
2247
- import Anu, { AbbreviateNumberOptions } from 'anu-verzum';
2248
-
2249
- const option: AbbreviateNumberOptions = {
2250
- // The abbreviations to use (for each 3 digits, starting with the first element):
2251
- units: [' E.', ' Mio.', ' Mrd.', ' T.'],
2252
- // How many decimal digits to preserve:
2253
- decimalPlaces: 3,
2254
- // Replace the default decimal sign (.) with a comma (,):
2255
- decimalSign: ','
2256
- };
2257
- Anu.Intl.abbreviateNumber(10000000000, option); // → "10 Mrd."
2258
- Anu.Intl.abbreviateNumber(100000000000, option); // → "100 Mrd."
2259
- Anu.Intl.abbreviateNumber(1000000000000, option); // → "1 T."
2260
- Anu.Intl.abbreviateNumber(-10000000, option); // → "-10 Mio."
2261
- Anu.Intl.abbreviateNumber(-10000, option); // → "-10 E."
2262
- Anu.Intl.abbreviateNumber(-10234, option); // → "-10,234 E."
2263
- ```
2264
-
2265
- <br>
2266
-
2267
- <h2 id="feature-api">Switching features on / off - <strong>The Feature API</strong></h2>
2268
-
2269
- - With feature toggle you can decide the circumstances a component should be rendered or not.<br>
2270
- This technique is typically used when some components should not be accessible due to lack of access rights or if the related backend logic is not implemented yet.<br>
2271
- Other typical use-case is when you have a feature but you don't want to show it in production yet because you want to test it carefully first.
2272
-
2273
- <h3 id="setting-features-list">Setting the features list</h3>
2274
-
2275
- - Set up an object with its keys as the name of the allowed features with a boolean value.
2276
- - Wrap the outermost element (this should contain all the features you want to toggle) within an <code>&lt;Anu.Feature.Provider /&gt;</code>.
2277
- - It takes a <code>features</code> property which should be the defined features list.
2278
-
2279
- ```typescript
2280
- const myFeaturesList: Record<string, boolean> = { myFeature: true };
2281
- const FeaturesWrapper = (): AnuElement => (
2282
- <Anu.Feature.Provider features={myFeaturesList}>
2283
- <Your_page />
2284
- </Anu.Feature.Provider>
2285
- );
2286
- ```
2287
-
2288
- <h3 id="toggling-features">Toggling the features</h3>
2289
-
2290
- - Wrap your component you want to render if the desired feature is set to <code>true</code> within <code>&lt;Anu.Feature.Toggle /&gt;</code>.
2291
- - It takes a <code>name</code> (string) property which should match with the name of the feature you added to the provider component.
2292
- If this property you refer with the value of the <code>name</code> attribute evaluates as true, the wrapped component can be rendered.
2293
- - You can also set an optional <code>defaultComponent</code>.
2294
- This will be rendered if the <code>name</code> evaluates as "falsy" (i.e.: either you set it to <code>false</code> or it wasn't even specified).
2295
- If <code>defaultComponent</code> is not set, <code>null</code> will be rendered instead.
2296
-
2297
- ```typescript
2298
- const ComponentWithFeatureToggle = (): AnuElement => {
2299
- const fallbackComponent = <div>You are not authorized to see this content...</div>;
2300
- return (
2301
- <Anu.Feature.Toggle name="myFeature" defaultComponent={fallbackComponent}>
2302
- <My_fancy_feature />
2303
- </Anu.Feature.Toggle>
2304
- );
2305
- };
2306
- ```
2307
-
2308
- <br>
2309
- <hr>
2310
-
2311
- <h1><strong><code>&lt;ANUVerzum /&gt;</code> JS</strong> UTILITIES:</h1>
2312
-
2313
- <br>
2314
-
2315
- <h2 id="deep-equal">Deep equality check for objects using <code>Anu.Utils.deepEqual()</code></h2>
2316
-
2317
- - The <code>Anu.Utils.deepEqual()</code> is a deep equality check utility that can check nested and complex objects if their structure and properties match (even if their references don't).
2318
-
2319
- ```typescript
2320
- const obj1: Record<string, any> = {
2321
- prop1: 'asdf',
2322
- prop2: true,
2323
- prop3: {
2324
- prop3_1: 'asdfasdf',
2325
- prop3_2: [{ itemProp1: 'item' }]
2326
- }
2327
- };
2328
- const obj2: Record<string, any> = {
2329
- prop1: 'asdf',
2330
- prop2: true,
2331
- prop3: {
2332
- prop3_1: 'asdfasdf',
2333
- prop3_2: [{ itemProp1: 'item' }]
2334
- }
2335
- };
2336
- const answer: boolean = Anu.Utils.deepEqual(obj1, obj2); // true
2337
- ```
2338
-
2339
- <br>
2340
- <hr>