flinker-dom 0.0.4 → 1.0.4

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 (2) hide show
  1. package/README.md +556 -2
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,7 +1,561 @@
1
- # Install
1
+ ## Intro
2
+ __FlinkerDom__ (FD) is a TypeScript library for building user interfaces and single-page web-applications.
3
+
4
+ __FD__
5
+ + uses all the power and flexibility of the [Flinker](https://github.com/Dittner/Flinker) FRP;
6
+
7
+ + does not use a virtual DOM;
8
+
9
+ + does not use TSX-files;
10
+
11
+ + uses dynamic CSS rules and CSS selectors caching, does not generate Inline Styles;
12
+
13
+ + is focused on updating each ui-component that is subscribed to changes of the RXObservable object.
14
+
15
+ ## Getting Started
16
+ 1. Install vite and create vanilla-ts template project:
17
+ ```cli
18
+ npm create vite@latest project-name -- --template vanilla-ts
19
+ ```
20
+
21
+ 2. Update package.json:
22
+ ```json
23
+ "dependencies": {
24
+ "flinker": "^2.0.4",
25
+ "flinker-dom": "^1.0.4"
26
+ }
27
+ ```
28
+
29
+ 3. Install dependencies:
30
+ ```cli
31
+ npm install
32
+ ```
33
+
34
+ 4. Let's write our simple application:
35
+ ```html
36
+ <!-- intex.html-->
37
+ <!DOCTYPE html>
38
+ <html lang="en">
39
+ <head>
40
+ ...
41
+ </head>
42
+ <body>
43
+ <div id="root"></div>
44
+ <script type="module" src="/src/index.ts"></script>
45
+ </body>
46
+ </html>
47
+ ```
48
+
49
+ ```ts
50
+ // index.ts
51
+ import './index.css'
52
+ import { App } from './App'
53
+
54
+ const app = App()
55
+ document.getElementById('root')!.appendChild(app.dom)
56
+
57
+ // App.ts
58
+ import { p } from 'flinker-dom'
59
+
60
+ export const App = () => {
61
+ return p().react(s => s.text = 'Hello, Flinker!')
62
+ }
63
+ ```
64
+
65
+ ## Example 1: Counter
66
+ ```ts
67
+ import { RXObservableValue } from 'flinker'
68
+ import { p, div, btn } from 'flinker-dom'
69
+
70
+ const Counter = () => {
71
+ // The function Counter will not be re-called by renderings.
72
+ // Therefore we can declare any functions and states
73
+ // in the Counter() body.
74
+ const rx = new RXObservableValue(0)
75
+
76
+ return div().children(() => {
77
+ p()
78
+ .observe(rx) // subscription to RXObservable object
79
+ .react(s => {
80
+ // react function will be called
81
+ // after the state (rx) changes
82
+ s.text = 'Count: ' + rx.value
83
+ s.textColor = '#222222'
84
+ })
85
+
86
+ // btn will not be re-rendered,
87
+ // because it is not subscribed to external state
88
+ btn()
89
+ .react(s => {
90
+ s.text = 'Inc'
91
+ s.textColor = '#ffFFff'
92
+ s.bgColor = '#222222'
93
+ s.cornerRadius = '4px'
94
+ s.padding = '10px'
95
+ })
96
+ .whenHovered(s => {
97
+ s.bgColor = '#444444'
98
+ })
99
+ .onClick(() => rx.value++)
100
+ })
101
+ }
102
+ ```
103
+
104
+ As a rule, we do not use attributes of functional components to specify properties. This example is incorrect:
105
+ ```ts
106
+ const Component = (props: { text: string, textColor: string }) => {
107
+ return p().react(s => {
108
+ s.text = props.text
109
+ s.textColor = props.textColor
110
+ })
111
+ }
112
+ ```
113
+
114
+ But we can use observable objects (RXObservable):
115
+ ```ts
116
+ const Counter = (rx: RXObservableValue<number>) => {
117
+ return p()
118
+ .observe(rx)
119
+ .react(s => ... )
120
+ }
121
+
122
+ const $state = new RXObservableValue(0)
123
+ Counter($state)
124
+ ```
125
+
126
+ ## Example 2: Inheritance
127
+ In order not to duplicate the style of our buttons, we must describe the style once with the ability to specify only the text prop and add handlers later.
128
+
129
+ ```ts
130
+ // Buttons.ts
131
+ export const ToggleBtn = ($isSelected: RXObservableValue<boolean>) => {
132
+ return btn()
133
+ .observe($isSelected)
134
+ .react(s => {
135
+ s.isSelected = $isSelected.value
136
+ s.textColor = '#ffFFff'
137
+ s.bgColor = '#222222'
138
+ s.cornerRadius = '5px'
139
+ s.padding = '10px'
140
+ })
141
+ .whenHovered(s => {
142
+ s.textColor = '#cc2222'
143
+ })
144
+ .whenSelected(s => {
145
+ s.bgColor = '#cc2222'
146
+ })
147
+ .onClick(() => {
148
+ $isSelected.value = !$isSelected.value
149
+ })
150
+ }
151
+
152
+ // Settings.ts
153
+ export class Settings {
154
+ readonly $rememberMe = new RXObservableValue(false)
155
+
156
+ constructor() {
157
+ this.$rememberMe.pipe()
158
+ .skipFirst() // ignore default false value
159
+ .onReceive(_ => {
160
+ this.storeSettings()
161
+ })
162
+ .subscribe()
163
+ }
164
+
165
+ private storeSettings() {
166
+ ...
167
+ }
168
+ }
169
+
170
+ // App.ts
171
+ const settings = new Settings()
172
+
173
+ const SettingsView = () => {
174
+ return vstack().children(() => {
175
+ ToggleBtn(settings.$rememberMe)
176
+ .react(s => s.text = 'Remember me')
177
+ })
178
+ }
179
+ ```
180
+
181
+ ## Example 3: Custom component
182
+ Let's create a button that has an icon and label.
183
+
184
+ ```ts
185
+ // Buttons.ts
186
+ import { btn, ButtonProps } from 'flinker-dom'
187
+
188
+ export interface IconBtnProps extends ButtonProps {
189
+ icon?: MaterialIcon
190
+ iconSize?: string
191
+ }
192
+
193
+ export const IconBtn = () => {
194
+ const $sharedState = new RXObservableValue<IconBtnProps>({})
195
+ return btn<IconBtnProps>()
196
+ // using propsDidChange handler,
197
+ // we can share btn-state to its children-components
198
+ .propsDidChange(props => $sharedState.value = props)
199
+ .react(s => {
200
+ s.display = 'flex'
201
+ s.flexDirection = 'row'
202
+ s.alignItems = 'center'
203
+ s.justifyContent = 'center'
204
+ s.gap = '5px'
205
+ s.wrap = false
206
+ s.boxSizing = 'border-box'
207
+ })
208
+ .children(() => {
209
+
210
+ //icon
211
+ $sharedState.value.icon && Icon()
212
+ .observe($sharedState) // subscription to the sharedState
213
+ .react(s => {
214
+ const ss = $sharedState.value
215
+ if (ss.icon) s.value = ss.icon
216
+ if (ss.iconSize) s.fontSize = ss.iconSize
217
+ s.textColor = 'inherit'
218
+ })
219
+
220
+ //label
221
+ $sharedState.value.text && span()
222
+ .observe($sharedState)
223
+ .react(s => {
224
+ const ss = $sharedState.value
225
+ s.text = ss.text
226
+ s.textColor = 'inherit'
227
+ s.fontSize = 'inherit'
228
+ s.fontFamily = 'inherit'
229
+ })
230
+ })
231
+ }
232
+ ```
233
+
234
+ In the example above we have used Icon as MaterialIcon:
235
+
236
+ ```ts
237
+ // Icons.ts
238
+ export interface IconProps extends TextProps {
239
+ value?: MaterialIcon
240
+ }
241
+
242
+ export const Icon = <P extends IconProps>() => {
243
+ return span<P>()
244
+ .react(s => {
245
+ s.value = MaterialIcon.question_mark // default icon
246
+ s.className = 'material_icon'
247
+ s.textSelectable = false
248
+ })
249
+ .map(s => s.text = s.value) // is called after react-functions
250
+ }
251
+
252
+ export enum MaterialIcon {
253
+ av_timer = 'av_timer',
254
+ autorenew = 'autorenew',
255
+ autofps_select = 'autofps_select',
256
+ auto_stories = 'auto_stories',
257
+ ...
258
+ zoom_out_map = 'zoom_out_map',
259
+ zoom_out = 'zoom_out',
260
+ zoom_in_map = 'zoom_in_map',
261
+ zoom_in = 'zoom_in',
262
+ }
263
+
264
+ // index.css
265
+ @font-face {
266
+ font-family: 'MaterialIcons';
267
+ font-style: normal;
268
+ font-weight: 400;
269
+ src: url('resources/fonts/MaterialIcons.ttf') format('truetype');
270
+ }
271
+
272
+ .material_icon {
273
+ font-family: 'MaterialIcons';
274
+ font-weight: normal;
275
+ font-style: normal;
276
+ font-size: 24px; /* Preferred icon size */
277
+ display: inline-block;
278
+ line-height: 1;
279
+ text-transform: none;
280
+ letter-spacing: normal;
281
+ word-wrap: normal;
282
+ white-space: nowrap;
283
+ direction: ltr;
284
+
285
+ /* Support for all WebKit browsers. */
286
+ -webkit-font-smoothing: antialiased;
287
+ /* Support for Safari and Chrome. */
288
+ text-rendering: optimizeLegibility;
289
+
290
+ /* Support for Firefox. */
291
+ -moz-osx-font-smoothing: grayscale;
292
+
293
+ /* Support for IE. */
294
+ -webkit-font-feature-settings: 'liga';
295
+ }
296
+ ```
297
+
298
+ As a result:
299
+
300
+ ```ts
301
+ // App.ts
302
+ IconBtn()
303
+ .react(s => {
304
+ s.icon = MaterialIcon.add
305
+ s.text = 'Btn with icon'
306
+ s.textColor = '#ffFFff'
307
+ s.bgColor = '#111111'
308
+ s.cornerRadius = '5px'
309
+ s.padding = '10px'
310
+ })
311
+ .whenHovered(s => {
312
+ s.textColor = '#cc2222'
313
+ s.bgColor = '#222222'
314
+ })
315
+ ```
316
+
317
+ ## Example 4: List
318
+ Lists manages re-rendering of its components. If we add to the end of the list a new component, the previous ones will not be re-created or re-rendered.
319
+
320
+ Let's create a simple ToDo App.
321
+
322
+ ```ts
323
+ // Model.ts
324
+ export interface Task {
325
+ id: number
326
+ text: string
327
+ }
328
+
329
+ export class ToDoModel {
330
+ readonly $tasks = new RXSubject<Task[], never>([])
331
+
332
+ private lastTaskId = 0
333
+ createTask(text: string) {
334
+ this.$tasks.value.push({ id: this.lastTaskId++, text })
335
+ this.$tasks.resend()
336
+ // using resend-method, all subscribers to the $tasks
337
+ // will be notified even if the $tasks.value remains the same.
338
+ // Therefore we are using RXSubject instead of RXObservableValue
339
+ }
340
+ }
341
+ ```
342
+
343
+ Our view contains a list of tasks:
344
+
345
+ ```ts
346
+ // App.ts
347
+ const model = new ToDoModel()
348
+
349
+ const TodoList = () => {
350
+ return vstack().children(() => {
351
+ vlist<Task>()
352
+ .observe(model.$tasks)
353
+ .items(() => model.$tasks.value) // will be re-called if model.$tasks changes
354
+ .itemRenderer(TaskView)
355
+
356
+ btn()
357
+ .react(s => {
358
+ s.bgColor = '#222222'
359
+ s.padding = '10px'
360
+ s.cornerRadius = '4px'
361
+ s.text = '+ New Task'
362
+ })
363
+ .onClick(() => {
364
+ model.createTask('New Task')
365
+ })
366
+ })
367
+ }
368
+
369
+ const TaskView = (t: Task) => {
370
+ return p()
371
+ .react(s => s.text = t.text)
372
+ }
373
+ ```
374
+
375
+ When model.$tasks changes vlist call items function to get tasks. Then vlist compares two lists of the tasks before and after changes. If different items are found for the same index, the previous component will be removed from the dom-tree and the new one will be added. By default, strict equality (===) is used to compare two elements. We can override this behavior, using equals method:
376
+
377
+ ```ts
378
+ vlist<Task>()
379
+ .observe(model.$tasks)
380
+ .items(() => model.$tasks.value)
381
+ .equals((a, b) => a.id === b.id)
382
+ .itemRenderer(TaskView)
383
+ ```
384
+
385
+ Vlist can be stylized as vstack:
386
+ ```ts
387
+ vlist<Task>()
388
+ .observe(model.$tasks)
389
+ .items(() => model.$tasks.value)
390
+ .itemRenderer(TaskView)
391
+ .react(s => {
392
+ s.width = '100%'
393
+ s.halign = 'left'
394
+ s.valign = 'center'
395
+ s.gap = '10px'
396
+ s.padding = '20px'
397
+ })
398
+ ```
399
+
400
+ ## Example 5: Affects
401
+ By observing changes we can clearly specify what reactions (ObserveAffect) should follow. We have three types of affects:
402
+
403
+ + __affectsProps__ (default) — only styles and props of the component will be updated, that has called an `observe`-method;
404
+ + __affectsChildrenProps__ — styles and props of the component's children will be updated including their children;
405
+ + __recreateChildren__ — old children will be removed from the dom-tree, and new ones will be added.
406
+
407
+ ### affectsChildrenProps case
408
+ There are states of change that affect only the properties and styles of nested components, not the structure. In this case, to avoid having to subscribe to changes in each child component, you can subscribe only in the parent one, specifying the affectsChildrenProps-affect. The application theme can act as such a state.
409
+
410
+ ```ts
411
+ export const App = () => {
412
+ const $theme = globalContext().app.$theme
413
+
414
+ return HomeView()
415
+ .observe($theme, 'affectsProps', 'affectsChildrenProps')
416
+ }
417
+ ```
418
+
419
+ Components support multiple observing, so we could write it like this:
420
+ ```ts
421
+ HomeView()
422
+ .observe($theme, 'affectsProps')
423
+ .observe($theme, 'affectsChildrenProps')
424
+ ```
425
+
426
+ ### recreateChildren case
427
+ Let's imagine that the user selects a document to view. Depending on the document, we may have different components structure. Therefore, we have to recreate the child components entirely.
428
+
429
+ ```ts
430
+ const DocView = () => {
431
+ const ctx = docContext()
432
+
433
+ return vstack()
434
+ .observe(ctx.$selectedDoc, 'recreateChildren')
435
+ .react(s => {
436
+ s.textColor = theme().text
437
+ s.gap = '0'
438
+ s.valign = 'top'
439
+ s.halign = 'left'
440
+ s.paddingVertical = '40px'
441
+ s.width = '100%'
442
+ }).children(() => {
443
+ // we always get an actual doc hier,
444
+ // since the children() method will be called
445
+ // every time $selectedDoc changes
446
+ const doc = ctx.$selectedDoc.value
447
+
448
+ DocInfo(doc)
449
+ DocHeader(doc)
450
+ DocBody(doc)
451
+
452
+ doc.isEditing && ToolBar(doc)
453
+ })
454
+ }
455
+ ```
456
+
457
+ If selectedDoc can be undefined, then we usually use the `observer` component:
458
+
459
+ ```ts
460
+ const DocView = ($doc: RXObservableValue<Doc | undefined>) => {
461
+ return observer($doc).onReceive(doc => {
462
+ return doc && vstack()
463
+ .react(s => ...)
464
+ .children(() => {
465
+
466
+ DocInfo(doc)
467
+ DocHeader(doc)
468
+ DocBody(doc)
469
+
470
+ doc.isEditing && ToolBar(doc)
471
+ })
472
+ })
473
+ }
474
+ ```
475
+
476
+ ## Example 6: Input
477
+ input and textarea components use binding mechanism for bidirectional text updating:
478
+
479
+ ```ts
480
+ const TextInput = ($input: RXObservableValue<string>) => {
481
+ return input()
482
+ .bind($input)
483
+ .react(s => {
484
+ // react will not be re-called if $input changes
485
+ s.type = 'text'
486
+ s.width = '100%'
487
+ s.height = '40px'
488
+ s.fontSize = theme().defFontSize
489
+ s.textColor = theme().text
490
+ s.bgColor = theme().inputBg
491
+ s.padding = '10px'
492
+ s.autoCorrect = 'off'
493
+ s.autoComplete = 'off'
494
+ s.borderBottom = '1px solid ' + theme().violet
495
+ })
496
+ .whenFocused(s => {
497
+ s.borderBottom = '1px solid ' + theme().red
498
+ })
499
+ .whenPlaceholderShown(s => {
500
+ s.textColor = '#666666'
501
+ })
502
+ }
503
+ ```
504
+
505
+ How is binding implemented?
506
+ ```ts
507
+ // FlinkerDom/src/components.ts
508
+ export class Input<P extends InputProps> extends UIComponent<P> {
509
+ bind(rx: RXObservableValue<string>) {
510
+ this.unsubscribeColl.push(
511
+ rx.pipe()
512
+ .onReceive(v => (this.dom as HTMLTextAreaElement).value = v)
513
+ .subscribe()
514
+ )
515
+
516
+ this.onInput((e: any) => rx.value = e.target.value)
517
+ return this
518
+ }
519
+ ...
520
+
521
+ onInput(callback: (event: Event) => void) {
522
+ this.dom.addEventListener('input', callback)
523
+ return this
524
+ }
525
+ }
526
+
527
+ export const input = <P extends InputProps>(type: InputType = 'text') => {
528
+ return new Input<P>('input').react(s => s.type = type)
529
+ }
530
+ ```
531
+
532
+ ## List of standard components (v.1.0):
533
+ + div
534
+ + p
535
+ + span
536
+ + h1
537
+ + h2
538
+ + h3
539
+ + h4
540
+ + h5
541
+ + h6
542
+ + btn
543
+ + link (a)
544
+ + switcher (div)
545
+ + observer (p hidden)
546
+ + vstack (div)
547
+ + hstack (div)
548
+ + vlist (div)
549
+ + hlist (div)
550
+ + spacer (div)
551
+ + image
552
+ + input
553
+ + textarea
554
+
555
+ ## Install
2
556
  ```cli
3
557
  npm i flinker-dom
4
558
  ```
5
559
 
6
- # License
560
+ ## License
7
561
  MIT
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "flinker-dom",
3
3
  "description": "Free TypeScript library for writing frontend apps",
4
- "version": "0.0.4",
4
+ "version": "1.0.4",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/Dittner/FlinkerDOM.git"