anu-verzum 1.0.0 → 1.3.0

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