forked from Volicon/Type-R
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.html
More file actions
2695 lines (2496 loc) · 199 KB
/
Copy pathindex.html
File metadata and controls
2695 lines (2496 loc) · 199 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>Type-R v4.0 API Reference</title>
<link rel="icon" href="docs/images/logo-dark.png" />
<link href="docs/lib/stylesheets/screen.css" rel="stylesheet" type="text/css" media="screen" />
<link href="docs/lib/stylesheets/print.css" rel="stylesheet" type="text/css" media="print" />
<link href="docs/lib/stylesheets/default.css" rel="stylesheet" type="text/css" />
<style>
.logo-section img {
vertical-align: middle;
margin: 15px;
height: 48px;
}
.logo-section .logo-text {
vertical-align: middle;
color: white;
display: inline-block;
}
.logo-section .logo-caption {
font-size: 28px;
}
</style>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
<script src="docs/lib/javascripts/all.js" type="text/javascript"></script>
<script>
$(function() {
var langs = [];
langs.push("javascript");
setupLanguages( langs );
});
</script>
</head>
<body class="index">
<a href="#" id="nav-button">
<span>
NAV
<img src="docs/images/navbar.png" />
</span>
</a>
<div class="tocify-wrapper">
<div class="logo-section">
<img src="docs/images/logo.png" />
<div class="logo-text">
<div class="logo-caption">Type-R v4.0</div>
<div>serializable type system</div>
</div>
</div>
<!--<div class="lang-selector">
<a href="#" data-language-name="javascript">javascript</a>
</div>-->
<div class="search">
<input type="text" class="search" id="input-search" placeholder="Search">
</div>
<ul class="search-results"></ul>
<div id="toc">
</div>
<ul class="toc-footer">
<li><a href="https://github.com/VoliJS/Type-R">GitHub repository</a></li>
<li><a href="https://github.com/VoliJS/Type-R/issues">Report the bug</a></li>
<li><a href="https://groups.google.com/forum/#!forum/volicon-open-source">Ask the question</a></li>
</ul>
</div>
<div class="page-wrapper">
<div class="content">
<p><img src="docs/images/overview.png" alt="overview"></p>
<h1 id="type-r-overview">Type-R Overview</h1>
<p>Type-R is a serializable type system for JS and TS. Data structures you describe with Type-R models are automatically and with zero effort:</p>
<ul>
<li>mapped to JSON and, optionally, REST API;</li>
<li>protected from improper updates at run-time;</li>
<li>deeply observable.</li>
</ul>
<h2 id="features">Features</h2>
<p>Mapping of complex JS types to JSON (such as Date, classes, objects trees with cross-references) is automatic with Type-R which eliminates a possibility of programmer's error and improves productivity. Less code to write means less things to unit test, less bugs to fix, and less code to read and understand when making changes.</p>
<p>Type-R models safeguard both frontend and backend from errors in JSON. Programmer's mistake on a frontend can't affect the JSON sent to the server. Wrong JSON received from the server will be validated, sanitized, and can't cause catastrophic failures on the frontend. Type-R guarantee that the data structures will retain the declared shape and it immediately reports improper assignments to the console.</p>
<p>There are virtually no point in unit-testing Type-R models as they are mostly declarative definitions. They are able to check the structural integrity themselves, and Type-R can be instructed to throw exceptions instead of console logging. It makes the unit tests of the data layer unnecessary, and greately reduces an effort when writing an integration test.</p>
<h2 id="react-integration">React integration</h2>
<p>Data structures defined with Type-R are deeply observable by default. They can be used to manage the state of React applications right out of box utilizing "unidirectional data flow" with no additional tooling. Type-R data structures support two-way data binding and attribute-level validation rules, making the a complex forms UI a trivial task. Normally, you don't change your UI code to add validation, just add the validation check to model's attributes.</p>
<h2 id="example">Example</h2>
<p>The main Type-R building block is the <code>Model</code> class with attributes types declaration which behaves as a regular JS class. Models and collections of models can be nested indefinitely to define data structures of arbitrary complexity.</p>
<pre><code class="highlight javascript"><span class="hljs-keyword">import</span> { define, Record, Collection } <span class="hljs-keyword">from</span> <span class="hljs-string">'@type-r/models'</span>
<span class="hljs-keyword">import</span> { restfulIO } <span class="hljs-keyword">from</span> <span class="hljs-string">'@type-r/endpoints'</span>
@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Record</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">name</span> : <span class="hljs-string">''</span>,
<span class="hljs-attr">email</span> : <span class="hljs-string">''</span>
}
}
@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Message</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Record</span> </span>{
<span class="hljs-keyword">static</span> endpoint = restfulIO( <span class="hljs-string">'/api/messages'</span>, {
<span class="hljs-comment">// REST I/O is simulated when the mock data is present, that's how you start.</span>
mockData : [ { <span class="hljs-attr">id</span> : <span class="hljs-number">0</span>, <span class="hljs-attr">createdAt</span> : <span class="hljs-string">"1999-07-25T03:33:29.687Z"</span>, <span class="hljs-attr">author</span> : {}, <span class="hljs-attr">to</span> : [] }]
} );
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">createdAt</span> : <span class="hljs-built_in">Date</span>,
<span class="hljs-attr">author</span> : User, <span class="hljs-comment">// aggregated User record.</span>
to : Collection.of( User ), <span class="hljs-comment">// aggregated collection of users</span>
subject : <span class="hljs-string">''</span>,
<span class="hljs-attr">body</span> : <span class="hljs-string">''</span>
}
}
<span class="hljs-keyword">const</span> messages = Collection.of( Message ).create();
<span class="hljs-keyword">await</span> messages.fetch({ <span class="hljs-attr">params</span> : { <span class="hljs-attr">page</span> : <span class="hljs-number">0</span> }});
<span class="hljs-keyword">const</span> msg = messages.first();
msg.author.name = <span class="hljs-string">'Alan Poe'</span>;
msg.subject = <span class="hljs-string">'Nevermore'</span>;
<span class="hljs-keyword">await</span> msg.save();
</code></pre>
<h2 id="api-reference-and-docs"><a href="https://volijs.github.io/Type-R/">API reference and docs</a></h2>
<h2 id="installation-and-requirements">Installation and requirements</h2>
<aside class="success">IE10+, Edge, Safari, Chrome, and Firefox are supported</aside>
<aside class="warning">IE9 and Opera may work but has not been tested. IE8 won't work.</aside>
<p>Install Type-R models and built-in set of I/O endpoints (restfulIO, localStorageIO, and memoryIO):</p>
<p><code>npm install @type-r/models @type-r/endpoints</code></p>
<p>Install React bindings:</p>
<p><code>npm install @type-r/react</code></p>
<p>Install extended data types (Email, URL, IP, Integer, Microsoft date, UNIX Timestamp date):</p>
<p><code>npm install @type-r/ext-types</code></p>
<h2 id="repository-structure">Repository structure</h2>
<ul>
<li><code>models</code> - Type-R framework core.</li>
<li><code>endpoints</code> - Type-R endpoints enabling models and collections I/O API.</li>
<li><code>react</code> - Type-R React bindings.</li>
<li><p><code>ext-types</code> - Extended data types.</p>
</li>
<li><p><code>globals</code> - <code>@type-r/globals</code> providing backward API compatibility for Type-R v2 apps.</p>
</li>
<li><p><code>mixture</code> - Events, Mixins, and log router. Used by <code>@type-r/models</code>.</p>
</li>
<li><code>tests</code> - private package containing all the unit tests.</li>
<li><code>examples/*</code> - example <code>@type-r/react</code> apps.</li>
</ul>
<h1 id="getting-started">Getting Started</h1>
<h2 id="local-state-and-models">Local state and models</h2>
<h3 id="const-state-attributes-attrs-">const State = attributes({...attrs})</h3>
<p><code>attributes()</code> function creates the "model" class out of the attributes definition, which is (in a simplest case) an object filled with default values. The shape of the generated class will repeat the shape of the attributes definition object.</p>
<pre><code class="highlight javascript"><span class="hljs-keyword">const</span> State = attributes({
<span class="hljs-attr">counters</span> : {
<span class="hljs-attr">a</span> : <span class="hljs-number">0</span>,
<span class="hljs-attr">b</span> : <span class="hljs-number">1</span>
}
})
</code></pre>
<h3 id="const-state-usemodel-state-">const state = useModel(State)</h3>
<p>Then, you can use it as a component's local state calling the <code>useModel</code> hook. When anything will change inside of the state, the component will be rendered. The state behaves like a plain mutable object with deeply observable changes.</p>
<pre><code class="highlight javascript"><span class="hljs-keyword">const</span> StatefulComponent = <span class="hljs-function"><span class="hljs-params">()</span> =></span> {
<span class="hljs-keyword">const</span> state = useModel( State );
<span class="hljs-keyword">return</span> (
<span class="xml"><span class="hljs-tag"><<span class="hljs-name">button</span> <span class="hljs-attr">onClick</span>=<span class="hljs-string">{</span> () =></span> state.counters.a++ }>
{ state.counters.a }
<span class="hljs-tag"></<span class="hljs-name">button</span>></span></span>
);
}
</code></pre>
<p>Attribute definition can be either:</p>
<ul>
<li>Default value (<code>0</code>, <code>""</code>, <code>false</code>)</li>
<li>Object <code>{ ...attrs }</code>, defining the nested model.</li>
<li>JS constructor designating the type (<code>Number</code>, <code>String</code>, <code>Boolean</code>, <code>Date</code>, other model's constructor, etc)</li>
<li>Array, meaning the collection of models <code>[{ ...attrs }]</code> or <code>[ModelConstructor]</code>.</li>
<li>Type and <code>null</code> default value (<code>type(Number).null</code>)</li>
<li>Arbitrary value and type (<code>type(Number).value(null)</code>)</li>
</ul>
<p>All attributes are typed, and all attributes have default values. If the default value is provided its constructor is being inferred from the value, and, in turn, if the constructor alone is given it's invoked to get the default value when the model is created. The following definitions have identical meaning: <code>0</code>, <code>Number</code>, <code>value(0)</code>, <code>type(Number)</code>, <code>type(Number).value(0)</code>. However, the type cannot be inferred from the <code>null</code> default value, which makes definitions like <code>type(Date).null</code> useful.</p>
<p>Attribute types <strong>are always checked and converted on assignment</strong>. <code>Boolean</code> attribute value will remain boolean no matter which value was assigned. The same applies to <code>String</code>, <code>Number</code>, and all other types. The model is garanteed to preserve its shape at run-time under any circumstances.</p>
<h2 id="linking-state-to-inputs">Linking state to inputs</h2>
<h3 id="linked-attributes">Linked attributes</h3>
<p>The writable reference to the attribute ("linked attribute") is obtained by prefixing its name with <code>$.</code>.
This reference is the instance of <code>Linked</code> class encapsulating the attribute's <code>value</code> and <code>set</code> function to update the value. Linked attributes can be passed to children as a single prop and used to create the data-bound input controls.</p>
<pre><code class="highlight javascript"><span class="hljs-keyword">const</span> State = attributes({
<span class="hljs-attr">post</span> : {
<span class="hljs-attr">title</span> : <span class="hljs-string">''</span>,
<span class="hljs-attr">body</span> : <span class="hljs-string">''</span>
}
});
<span class="hljs-keyword">const</span> Form = <span class="hljs-function"><span class="hljs-params">()</span> =></span> {
<span class="hljs-keyword">const</span> state = useModel( State );
<span class="hljs-keyword">return</span> (
...
<Input type=<span class="hljs-string">"text"</span> $value={state.post.$.title}/>
...
)
}
<span class="hljs-keyword">const</span> Input = <span class="hljs-function">(<span class="hljs-params">{ $value, ...rest }</span>) =></span>
<span class="xml"><span class="hljs-tag"><<span class="hljs-name">input</span>
<span class="hljs-attr">value</span>=<span class="hljs-string">{$value.value}</span>
<span class="hljs-attr">onChange</span>=<span class="hljs-string">{</span> <span class="hljs-attr">e</span> =></span> $value.set( e.target.value )}
/></span>
</code></pre>
<h3 id="ad-hoc-data-binding">Ad-hoc data binding</h3>
<p>Custom components are not required to use data binding. <code>Linked</code> class has the <code>.props</code> helper to create <code>value</code> and <code>onChange</code> props consumed by standard HTML inputs:</p>
<pre><code class="highlight javascript"><input type=<span class="hljs-string">"text"</span> {...state.post.$.title.props}/>
</code></pre>
<p>However, custom input component are highly useful to encapsulate styles, and are recommended.</p>
<h3 id="validation-in-components">Validation in components</h3>
<p><code>Linked</code> class contains <code>error</code> property which is populated by the <code>check</code> method. Checks have a form of asserts, and they can be chained. The first failing check will populate the error, skipping all the rest.</p>
<p>Error value in the linked attribute can be displayed by the custom input controls.</p>
<pre><code class="highlight javascript"><span class="hljs-keyword">const</span> State = attributes({
<span class="hljs-attr">name</span> : <span class="hljs-string">''</span>,
<span class="hljs-attr">email</span> : <span class="hljs-string">''</span>
})
<span class="hljs-keyword">const</span> Page = <span class="hljs-function"><span class="hljs-params">()</span> =></span> {
<span class="hljs-keyword">const</span> state = useState( State );
<span class="hljs-comment">// Validate linked attributes.</span>
state.$.name
.check( <span class="hljs-function"><span class="hljs-params">x</span> =></span> x, <span class="hljs-string">'Required'</span> );
state.$.email
.check( <span class="hljs-function"><span class="hljs-params">x</span> =></span> x, <span class="hljs-string">'Required'</span> )
.check( <span class="hljs-function"><span class="hljs-params">x</span> =></span> x.indexOf(<span class="hljs-string">'@'</span>) > <span class="hljs-number">0</span>, <span class="hljs-string">'Should contain @'</span> );
<span class="hljs-keyword">return</span> (
<div>
<label>Name:
<Input $value={state.$.name}/>
</label>
<label>Email:
<Input $value={state.$.email}/>
</label>
</div>
)
}
// Input component indicating the validation error.
const Input = ({ $value, className = "", ...rest }) =>
<input
className={ $value.error ? className + ' error' : className }
value={$value.value}
onChange={ e => $value.set( e.target.value )}
/>
</code></pre>
<h3 id="validation-in-state">Validation in state</h3>
<p>Validation checks can be attached directly to the attributes. To do that, wrap the attribute's default value in <code>value('')</code>, or the attribute's type in <code>type(String)</code>. Validation is attached with a similar <code>check()</code> call, or a pre-defined <code>required</code> shortcut checking for falsy values. This technique can be combined with validation in the React components described above.</p>
<pre><code class="highlight javascript"><span class="hljs-keyword">const</span> State = attributes({
<span class="hljs-attr">name</span> : value(<span class="hljs-string">''</span>).required,
<span class="hljs-attr">email</span> : value(<span class="hljs-string">''</span>)
.required
.check( <span class="hljs-function"><span class="hljs-params">x</span> =></span> x.indexOf(<span class="hljs-string">'@'</span>) > <span class="hljs-number">0</span>, <span class="hljs-string">'Email must contain @'</span> )
})
</code></pre>
<p>In this case, validation will happen transarently in the moment when the attribute is accessed though <code>$</code> and the error will be attached to the linked attribute. The validation result is cached together with the <code>Linked</code> class, and the validation won't happen again unless the attribute value will change.</p>
<p>Also, it's easy to check if the whole state or its part is valid calling the <code>isValid()</code> method on state or it's nested object. In this case, the cosserponding state part will be validated recursively checking all the attributes.</p>
<p>Attribute's validation can be reused in a variety of ways. Validation function can encapsulate the error message and used in <code>check</code>.</p>
<pre><code class="highlight javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isEmail</span>(<span class="hljs-params"> x </span>)</span>{
<span class="hljs-keyword">return</span> <span class="hljs-function"><span class="hljs-params">x</span> =></span> x.indexOf(<span class="hljs-string">'@'</span>) > <span class="hljs-number">0</span>;
}
isEmail.error = <span class="hljs-string">'Email must contain @'</span>;
<span class="hljs-keyword">const</span> State = attributes({
<span class="hljs-attr">name</span> : value(<span class="hljs-string">''</span>).required,
<span class="hljs-attr">email</span> : value(<span class="hljs-string">''</span>).check( isEmail )
})
</code></pre>
<p>Or, a developer can reuse the whole attribute definition.</p>
<pre><code class="highlight javascript"><span class="hljs-keyword">const</span> Email = value(<span class="hljs-string">''</span>)
.required
.check( <span class="hljs-function"><span class="hljs-params">x</span> =></span> x.indexOf(<span class="hljs-string">'@'</span>) > <span class="hljs-number">0</span>, <span class="hljs-string">'Email must contain @'</span> );
<span class="hljs-keyword">const</span> State = attributes({
<span class="hljs-attr">name</span> : value(<span class="hljs-string">''</span>).required,
<span class="hljs-attr">email</span> : Email
})
</code></pre>
<h2 id="rendering-lists-and-collections">Rendering lists and collections</h2>
<p>Attribute is a collection if it's definition is an object enclosed in an array.</p>
<pre><code class="highlight javascript"><span class="hljs-keyword">const</span> State = attributes({
<span class="hljs-attr">users</span> : [{ <span class="hljs-comment">// Collection of users.</span>
name : <span class="hljs-string">''</span>,
<span class="hljs-attr">email</span> : <span class="hljs-string">''</span>
}]
})
</code></pre>
<p>When the state will be created, the collection will be represented with a <code>Collection</code> class having the most popular methods of <code>Array</code> class (<code>map</code>, <code>reduce</code>, <code>filter</code>, <code>push</code>, <code>slice</code>...) and BackboneJS collections API (<code>get</code>, <code>set</code>, <code>add</code>, <code>remove</code>, <code>groupBy</code>).</p>
<p>Model instances have a unique auto-generated <code>cid</code> ("client id") property which is helpful when rendering lists.</p>
<pre><code class="highlight javascript"><span class="hljs-keyword">const</span> Users = <span class="hljs-function"><span class="hljs-params">()</span> =></span> {
<span class="hljs-keyword">const</span> state = useModel(State);
<span class="hljs-keyword">return</span> (
<span class="xml"><span class="hljs-tag"><<span class="hljs-name">div</span>></span>
{ state.users.map( user =>
<span class="hljs-tag"><<span class="hljs-name">User</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{user.cid}</span> <span class="hljs-attr">model</span>=<span class="hljs-string">{user}</span> /></span>
)}
<span class="hljs-tag"></<span class="hljs-name">div</span>></span>
)
}</span>
</code></pre>
<p>Collection items, as well all nested models and collections, can be passed to children components as values. No wrapping in linked $-attributes is needed. Assuming that we're using out data-bound <code>Input</code> component from the previous section, it will look like this:</p>
<pre><code class="highlight javascript"><span class="hljs-keyword">const</span> User = <span class="hljs-function">(<span class="hljs-params">{ user }</span>) =></span>
<div>
<label>
Name:
<Input $value={user.$.name}/>
</label>
<label>
Email:
<Input $value={user.$.email}/>
</label>
</div>
</code></pre>
<blockquote>
<p>Internally, collection is a combination of models array (<code>models</code>) and the hash-map index by the model's <code>id</code> and <code>cid</code> attributes used internally by the <code>get(id)</code> and other methods. As it proxies popular <code>Array</code> methods, accessing the <code>models</code> directly is rarely needed.</p>
</blockquote>
<h2 id="managing-the-complex-state">Managing the complex state</h2>
<p>TODO: move the info about attribute types here.</p>
<p>When the state becomes complex, it's definition grows and parts of it often repeats itself. When it will happen, you can split the state definition to parts and compose your state out of them.
These parts can be reused across different components and in different contexts.</p>
<p>To do that, simply use the generated model class in place of the attribute's default value.</p>
<pre><code class="highlight javascript"><span class="hljs-comment">// models.js</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> User = attributes({
<span class="hljs-attr">name</span> : <span class="hljs-string">''</span>,
<span class="hljs-attr">email</span>: <span class="hljs-string">''</span>
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Post = attributes({
<span class="hljs-attr">author</span> : User
title : <span class="hljs-string">''</span>,
<span class="hljs-attr">body</span> : <span class="hljs-string">''</span>
})
<span class="hljs-comment">// page.js</span>
<span class="hljs-keyword">import</span> { User } <span class="hljs-keyword">from</span> <span class="hljs-string">'./models'</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">State</span> </span>= attributes({
<span class="hljs-attr">posts</span> : [User]
});
<span class="hljs-keyword">const</span> Page = <span class="hljs-function"><span class="hljs-params">()</span> =></span> {
<span class="hljs-keyword">const</span> state = useModel(State);
<span class="hljs-keyword">return</span> (
<span class="xml"><span class="hljs-tag"><<span class="hljs-name">div</span>></span>
{ state.posts.map( x => <span class="hljs-tag"><<span class="hljs-name">EditPost</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{x.cid}</span> <span class="hljs-attr">post</span>=<span class="hljs-string">{x}</span> /></span> )}
<span class="hljs-tag"></<span class="hljs-name">div</span>></span>
)
}</span>
</code></pre>
<h2 id="fetching-the-data">Fetching the data</h2>
<h3 id="defining-the-endpoints">Defining the endpoints</h3>
<pre><code class="highlight javascript"><span class="hljs-keyword">import</span> { attributes, metadata } <span class="hljs-keyword">from</span> <span class="hljs-string">'@type-r/models'</span>
<span class="hljs-keyword">import</span> { restfulIO } <span class="hljs-keyword">from</span> <span class="hljs-string">'@type-r/endpoints'</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Users = attributes({
<span class="hljs-attr">name</span> : <span class="hljs-string">''</span>,
<span class="hljs-attr">email</span> : <span class="hljs-string">''</span>,
[metadata] : {
<span class="hljs-attr">endpoint</span> : restfulIO(<span class="hljs-string">'/api/users'</span>)
}
})
</code></pre>
<h3 id="doing-the-i-o">Doing the I/O</h3>
<pre><code class="highlight javascript"><span class="hljs-keyword">const</span> State = attributes({
<span class="hljs-attr">users</span> : [Users]
})
<span class="hljs-keyword">const</span> Page = <span class="hljs-function"><span class="hljs-params">()</span> =></span> {
<span class="hljs-keyword">const</span> state = useModel(State);
<span class="hljs-keyword">const</span> isReady = useIO(<span class="hljs-keyword">async</span> () =>{
<span class="hljs-keyword">await</span> state.users.fetch();
});
<span class="hljs-keyword">return</span> isReady ?
<span class="xml"><span class="hljs-tag"><<span class="hljs-name">UsersList</span> <span class="hljs-attr">users</span>=<span class="hljs-string">{state.users}/</span>></span>
:
<span class="hljs-tag"><<span class="hljs-name">div</span>></span>Loading...<span class="hljs-tag"></<span class="hljs-name">div</span>></span>
}</span>
</code></pre>
<h3 id="todo-bearer-auth">TODO: Bearer Auth</h3>
<h2 id="managing-the-simple-state">Managing the simple state</h2>
<h3 id="uselinked-value-">useLinked( value )</h3>
<pre><code class="highlight javascript"><span class="hljs-keyword">const</span> StatefulDataBound = <span class="hljs-function"><span class="hljs-params">()</span> =></span> {
<span class="hljs-comment">// Obtain linked local state.</span>
<span class="hljs-keyword">const</span> $name = useLinked( <span class="hljs-string">''</span> );
<span class="hljs-keyword">return</span> (
<span class="xml"><span class="hljs-tag"><<span class="hljs-name">div</span>></span>
<span class="hljs-tag"><<span class="hljs-name">input</span> {<span class="hljs-attr">...</span>$<span class="hljs-attr">name.props</span>} /></span>
<span class="hljs-tag"></<span class="hljs-name">div</span>></span>
)
}
</span>
</code></pre>
<h3 id="usecollection-of-modelclass-">useCollection.of( ModelClass )</h3>
<pre><code class="highlight javascript"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Counter</span> </span>= attributes({
<span class="hljs-attr">counter</span> : <span class="hljs-number">0</span>
});
<span class="hljs-keyword">const</span> StatefulComponent = <span class="hljs-function"><span class="hljs-params">()</span> =></span> {
<span class="hljs-keyword">const</span> counters = useCollection.of( Counter );
<span class="hljs-keyword">const</span> selected = useCollection.subsetOf( counters );
<span class="hljs-keyword">return</span> (
<span class="xml"><span class="hljs-tag"><<span class="hljs-name">div</span>></span>
<span class="hljs-tag"><<span class="hljs-name">div</span>></span>{ user.counter }<span class="hljs-tag"></<span class="hljs-name">div</span>></span>
<span class="hljs-tag"><<span class="hljs-name">button</span> <span class="hljs-attr">onClick</span>=<span class="hljs-string">{</span> () =></span> user.counter++ }>Add<span class="hljs-tag"></<span class="hljs-name">button</span>></span>
<span class="hljs-tag"></<span class="hljs-name">div</span>></span></span>
}
</code></pre>
<h2 id="remove-normalized-data-and-stores">REMOVE Normalized data and stores</h2>
<p><code>Store</code> is the subclass of <code>Model</code> used as a root to resolve id-references in 'normalized data structures', when
the data is represented as a set of collections with items referencing each other by id. If you don't have normalized data structures, you don't need <code>Store</code>.</p>
<p>Attributes of types <code>Model.memberOf( 'store.someCollection' )</code> and <code>Collection.subsetOf( 'store.someCollection' )</code>
will resolve model ids to the models taken from <code>someCollection</code> belonging to the closest <code>Store</code> model. The closest
store is located as follows:</p>
<p>1) The first <code>Store</code> from the model's owners chain is taken first.
2) If there are no such a collection in it, the next <code>Store</code> class in ownership chain is taken.
3) If there are no stores left in the ownerhip chain, the <code>Store.global</code> is used.</p>
<p>From the particular model's view, there's a single <code>store</code> namespace which is defined by <code>Store.global</code> and
extended by upper stores in its ownership chain.</p>
<p>In <code>@type-r/react</code>, you can create the store as a local component state, and expose it down to the component subtree
so its children can opt to use this context store for id resolutions in their local state models.</p>
<p>That leads to a multi-tier store achitecture where the next tier store may override upper store collections and extend it with new collections.</p>
<ul>
<li>Tier 1. <code>Store.global</code> holds the state which is shared across all SPA pages.</li>
<li>Tier 2. Page component stores holds the state which is related to particular pages.</li>
<li>Tier 3. Particular components might add their local stores extending the namespace created by upper stores.</li>
</ul>
<p>Stores</p>
<pre><code class="highlight javascript"><span class="hljs-keyword">const</span> X = <span class="hljs-function"><span class="hljs-params">()</span> =></span> {
<span class="hljs-keyword">const</span> state = useModel( State );
useContextStore( state );
}
</code></pre>
<h1 id="defining-the-model">Defining the model</h1>
<h2 id="overview">Overview</h2>
<p><code>Model</code> is a class with attributes which:</p>
<ul>
<li>is deeply observable;</li>
<li>is automatically serializable to JSON;</li>
<li>can be mapped to REST endpoint using the declarative annotations;</li>
<li>is protected from improper updates, checking types at run-time;</li>
<li>is very fast, easily handling large collections of deeply nested objects (10K and more).</li>
</ul>
<p>Models are statically typed when used with TypeScript, and much safer than standard classes when used with JavaScript. When used to describe REST endpoints, it augment TypeScript with a dynamic type checks guarding the client-server protocol against errors on both ends.</p>
<p>Model declarations looks close to the shape of JSON objects they describe.</p>
<pre><code class="highlight javascript"><span class="hljs-comment">// Create Role model's constructor.</span>
<span class="hljs-keyword">const</span> Role = attributes({
<span class="hljs-attr">createdAt</span> : <span class="hljs-built_in">Date</span>, <span class="hljs-comment">// date, represented as UTC string in JSON.</span>
name : <span class="hljs-built_in">String</span> <span class="hljs-comment">// A string. Guaranteed.</span>
});
<span class="hljs-comment">// Create Role model's constructor.</span>
<span class="hljs-keyword">const</span> User = attributes({
<span class="hljs-attr">name</span> : <span class="hljs-built_in">String</span>,
<span class="hljs-attr">isActive</span> : <span class="hljs-built_in">Boolean</span>, <span class="hljs-comment">// always boolean, no matter what is assigned.</span>
<span class="hljs-comment">// Nested collection of roles, represented as an array of objects in JSON.</span>
roles : [Role],
<span class="hljs-comment">// Nested model, represented as nested object in JSON.</span>
permissions : {
<span class="hljs-attr">canDoA</span> : <span class="hljs-built_in">Boolean</span>,
<span class="hljs-attr">canDoB</span> : <span class="hljs-built_in">Boolean</span>,
}
});
<span class="hljs-keyword">const</span> user = <span class="hljs-keyword">new</span> User();
user.onChanges( <span class="hljs-function"><span class="hljs-params">()</span> =></span> <span class="hljs-built_in">console</span>.log( <span class="hljs-string">'change'</span> ));
user.permissions.canDoA = <span class="hljs-literal">true</span>;
</code></pre>
<p>Models may have I/O endpoints attached. There are several endpoints awailable in <code>@type-r/endpoints</code> package, including the standard REST endpoint.</p>
<pre><code class="highlight javascript"><span class="hljs-comment">// Use the class form of the model definition.</span>
<span class="hljs-keyword">const</span> User = attributes({
[metadata] : {
<span class="hljs-comment">// Bind the REST endpoint to enable I/O API.</span>
endpoint : restfulIO( <span class="hljs-string">'/api/users'</span> )
}
name : <span class="hljs-built_in">String</span>
}
<span class="hljs-comment">// Fetch the users list.</span>
<span class="hljs-keyword">const</span> users = <span class="hljs-keyword">await</span> Collection.of( User ).create().fetch();
<span class="hljs-keyword">const</span> firstUser = users.first();
firstUser.isActive = <span class="hljs-literal">true</span>;
<span class="hljs-keyword">await</span> firstUser.save();
</code></pre>
<p>Model observe changes in its attributes, including the changes in nested models and collections. Listeners can subscribe and unsubscribe for change events, which makes it easy to integrate models with virtually any view layer.</p>
<pre><code class="highlight javascript"><span class="hljs-comment">// Subscribe for the changes...</span>
users.onChanges( <span class="hljs-function"><span class="hljs-params">()</span> =></span> <span class="hljs-built_in">console</span>.log( <span class="hljs-string">'changes!'</span> ) );
users.first().name = <span class="hljs-string">"another name"</span>;
<span class="hljs-comment">// changes!</span>
</code></pre>
<p>All aspects of model behavior can be controlled on the attribute level through the attribute metadata. It makes it easy to define reusable attribute types with custom serialization, validation, and reactions on changes.</p>
<pre><code class="highlight javascript"> <span class="hljs-comment">// Email attribute is a string...</span>
<span class="hljs-keyword">const</span> Email = type( <span class="hljs-built_in">String</span> )
<span class="hljs-comment">// ...having @ symbol in it.</span>
.check( <span class="hljs-function"><span class="hljs-params">x</span> =></span> !x || x.indexOf( <span class="hljs-string">'@'</span> ) >= <span class="hljs-number">0</span>, <span class="hljs-string">'Must be an email'</span> );
<span class="hljs-keyword">const</span> User = attributes({
<span class="hljs-attr">email</span> : Email.required <span class="hljs-comment">// Has not be empty to be valid.</span>
<span class="hljs-comment">// Date which is `null` by default, read it from JSON, but don't save it back.</span>
createdAt : type( <span class="hljs-built_in">Date</span> ).null.dontSave,
<span class="hljs-comment">//...</span>
})
</code></pre>
<p>There are four sorts of model attributes:</p>
<ul>
<li><strong>Primitive</strong> types (Number, String, Boolen) mapped to JSON directly.</li>
<li><strong>Immutable</strong> types (Date, Array, Object, or custom immutable class).</li>
<li><strong>Nested</strong> models and collections represented as nested objects and arrays of objects in JSON.</li>
<li><strong>Referenced</strong> models and collections. References can be:<ul>
<li><strong>serializable</strong> to JSON as an ids of the referenced models, used to model one-to-many and many-to-many relashinships in JSON;</li>
<li><strong>observable</strong>, which is a non-persistent run-time only reference, used to model temporary application state.</li>
</ul>
</li>
</ul>
<h3 id="attributes-attrdefs-">attributes( attrDefs )</h3>
<p>Create the Model class constructor from the attribute definitions.</p>
<h3 id="decorator-define"><code>decorator</code> @define</h3>
<p>Class decorator which must preceede the <code>Model</code> subclass declaration. <code>@define</code> is a mixin which will read attribute definitions from the Model's <code>static attributes</code> and generate class properties accessors accordingly.</p>
<p><code>@define</code> assembles attribute's update pipeline for each attribute individually depending on its type and employs a number of JIT-friendly optimizations. As a result, Type-R models handle updates about 10 times faster than frameworks like BackboneJS in all popular browsers making a collections of 10K objects a practical choice on a frontend.</p>
<h3 id="static-attributes"><code>static</code> attributes</h3>
<p>Attributes must be defined in <code>static attributes</code>. In a majority of cases, an attribute definition is a constructor function or the default value.</p>
<p>If the function is used as attribute definition, it's assumed to be am attributes constructor and designates attribute type. If it's not a function, it's treated as an attribute's default value and type is being determened from this value type. The following attribute definitions are equivalent:</p>
<pre><code class="highlight javascript">@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">name</span> : <span class="hljs-built_in">String</span>, <span class="hljs-comment">// Same as ''</span>
email : <span class="hljs-string">''</span> <span class="hljs-comment">// Same as String</span>
}
}
</code></pre>
<p>To assign <code>null</code> as a default value both attribute type and value need to be specified, as the type cannot be inferred from <code>null</code>. It's done through the attribute's metadata like this:</p>
<pre><code>nullStringAttr : type( <span class="hljs-built_in">String</span> ).value( <span class="hljs-literal">null</span> )
</code></pre><h3 id="static-idattribute-attrname-"><code>static</code> idAttribute = 'attrName'</h3>
<p>A model's unique identifier is stored under the pre-defined <code>id</code> attribute.
If you're directly communicating with a backend (CouchDB, MongoDB) that uses a different unique key, you may set a Model's <code>idAttribute</code> to transparently map from that key to id.</p>
<p>Model's <code>id</code> property will still be linked to Model's id, no matter which value <code>idAttribute</code> has.</p>
<pre><code class="highlight javascript">@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Meal</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-keyword">static</span> idAttribute = <span class="hljs-string">"_id"</span>;
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">_id</span> : <span class="hljs-built_in">Number</span>,
<span class="hljs-attr">name</span> : <span class="hljs-string">''</span>
}
}
<span class="hljs-keyword">const</span> cake = <span class="hljs-keyword">new</span> Meal({ <span class="hljs-attr">_id</span>: <span class="hljs-number">1</span>, <span class="hljs-attr">name</span>: <span class="hljs-string">"Cake"</span> });
alert(<span class="hljs-string">"Cake id: "</span> + cake.id);
</code></pre>
<h3 id="static-endpoint"><code>static</code> endpoint</h3>
<p>Enable model's I/O API by specifying an I/O endpoint. There are the list of endpoints in <code>@type-r/enpoints</code> package to work with browsers local storage, REST, and mock data.</p>
<h2 id="primitive-attributes">Primitive attributes</h2>
<p>Primitive attribute types are directly mapped to their values in JSON.
Assigned value is being converted to the declared attribute type at run time. I.e. if an email is declared to be a string, it's guaranteed that it will always remain a string.</p>
<pre><code class="highlight javascript">@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">name</span> : <span class="hljs-string">''</span>,
<span class="hljs-attr">email</span> : <span class="hljs-built_in">String</span>, <span class="hljs-comment">// ''</span>
isActive : <span class="hljs-built_in">Boolean</span>, <span class="hljs-comment">// false</span>
failedLoginCount : <span class="hljs-built_in">Number</span> <span class="hljs-comment">// 0</span>
}
}
</code></pre>
<h3 id="attribute-number"><code>attribute</code> : Number</h3>
<p>JS <code>number</code> primitive type. Assigned value (except <code>null</code>) is automatically converted to <code>number</code> with a constructor call <code>Number( value )</code>.</p>
<p>If something other than <code>null</code>, number, or a proper string representation of number is being assigned, the result of the convertion is <code>NaN</code> and the warning
will be displayed in the console. Models with <code>NaN</code> in their <code>Number</code> attributes will fail the validation check.</p>
<h3 id="attribute-boolean"><code>attribute</code> : Boolean</h3>
<p>JS <code>boolean</code> primitive type. Assigned value (except <code>null</code>) is automatically converted to <code>true</code> or <code>false</code> with a constructor call <code>Boolean( value )</code>.</p>
<p>This attribute type is always valid.</p>
<h3 id="attribute-string"><code>attribute</code> : String</h3>
<p>JS <code>string</code> primitive type. Assigned value (except <code>null</code>) is automatically converted to <code>string</code> with a constructor call <code>String( value )</code>.</p>
<p>This attribute type is always valid.</p>
<h2 id="immutable-attributes">Immutable attributes</h2>
<h3 id="attribute-date"><code>attribute</code> : Date</h3>
<p>JS <code>Date</code> type represented as ISO UTC date string in JSON. If assigned value is not a <code>Date</code> or <code>null</code>, it is automatically converted to <code>Date</code> with a constructor call <code>new Date( value )</code>.</p>
<p>If something other than the integer timestamp or the proper string representation of date is being assigned, the result of the convertion is <code>Invalid Date</code> and the warning
will be displayed in the console. Models with <code>Invalid Date</code> in their <code>Date</code> attributes will fail the validation check.</p>
<p>Note that the changes to a <code>Date</code> attribute are not observable; dates are treated as immutables and need to be replaced for the model to notice the change.</p>
<pre><code class="highlight javascript">@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">name</span> : <span class="hljs-string">''</span>,
<span class="hljs-attr">email</span> : <span class="hljs-built_in">String</span>, <span class="hljs-comment">// ''</span>
createdAt : <span class="hljs-built_in">Date</span>
}
}
</code></pre>
<h3 id="attribute-array"><code>attribute</code> : Array</h3>
<p>Immutable JS Array mapped to JSON as is. Type-R assumes that an Array attribute contains a raw JSON with no complex data types in it. </p>
<p><code>Array</code> type is primarily used to represent a list of primitives. It's recommended to use aggregated collections of models for the array of objects in JSON.</p>
<p>If an assigned value is not an <code>Array</code>, the assignment will be ignored and a warning will be displayed in the console.
Array attributes are always valid.</p>
<p>Note that the changes to an <code>Array</code> attribute are not observable; arrays need to be replaced with an updated copy for the model to notice the change. Type-R uses <code>Linked</code> class proxying popular array methods from <code>@linked/value</code> package to simplify manipulations with immutable arrays.</p>
<pre><code class="highlight javascript">@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">name</span> : <span class="hljs-built_in">String</span>,
<span class="hljs-attr">roles</span> : [ <span class="hljs-string">'admin'</span> ]
}
}
user.$.roles.push( <span class="hljs-string">'user'</span> );
</code></pre>
<h3 id="attribute-object"><code>attribute</code> : Object</h3>
<p>Immutable JS Object mapped to JSON as is. Type-R assumes that an Object attribute contains a raw JSON with no complex data types in it. </p>
<p>Plain JSON object type primarily used to represent dynamic hashmaps of primitives. It's recommended to use aggregated models for the complex nested objects in JSON.</p>
<p>If an assigned value is not a plain object, the assignment will be ignored and the warning will be displayed in the console. Object attributes are always valid.</p>
<p>Changes in Object attribute are not observable, object needs to be copied for the Type-R to notice the change. Type-R uses <code>Linked</code> class from <code>@linked/value</code> package to simplify manipulations with immutable objects.</p>
<pre><code class="highlight javascript">@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">name</span> : <span class="hljs-built_in">String</span>,
<span class="hljs-attr">roles</span> : { <span class="hljs-attr">admin</span> : <span class="hljs-literal">true</span> }
}
}
user.$.roles.at( <span class="hljs-string">'user'</span> ).set( <span class="hljs-literal">false</span> );
</code></pre>
<h3 id="attribute-function"><code>attribute</code> : Function</h3>
<p>Function as an attribute value. Please note that functions are not serializable.</p>
<p>Function attributes are initialized with an empty function by default. If something other than function will be assigned, it will be ignored with a error in a console.</p>
<h3 id="attribute-classconstructor"><code>attribute</code> : ClassConstructor</h3>
<p>If the class constructor used as an attribute type and it's not a model or collection subclass, it is considered to be an <strong>immutable attribute</strong>. Type-R has following assumptions on immutable attributes class:</p>
<ul>
<li>It has <code>toJSON()</code></li>
<li>Its constructor can take JSON as a single argument.</li>
</ul>
<p>Changes in immutable attributes <em>are not observable</em>, object needs to be replaced with its updated copy for the model to notice the change. The class itself doesn't need to be immutable, though, as Type-R makes no other assumptions.</p>
<h2 id="nested-models">Nested models</h2>
<p>Nested models are the part of the model represented in JSON as nested objects. Nested models <strong>will</strong> be copied, destroyed, and validated as a part of the parent.</p>
<p>Model has an exclusive ownership rights on its nested members. Nested model can't be assigned to another model's attribute unless the source attribute is cleared or the target attribute has a reference type.</p>
<p>Nested models are deeply observable. A change of the nested model's attributute will trigger the <code>change</code> event on its parent.</p>
<h3 id="attribute-modelclass"><code>attribute</code> : ModelClass</h3>
<p>Nested model. Describes an attribute represented in JSON as an object.</p>
<ul>
<li>Attribute <strong>is</strong> serializable as <code>{ attr1 : value1, attr2 : value2, ... }</code></li>
<li>Changes of enclosed model's attributes <strong>will not</strong> trigger change of the model.</li>
</ul>
<pre><code class="highlight javascript"><span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">users</span> : Collection.of( User ),
<span class="hljs-attr">selectedUser</span> : memberOf( <span class="hljs-string">'users'</span> )
}
</code></pre>
<h3 id="attribute-collection-of-modelclass-"><code>attribute</code> : Collection.of( ModelClass )</h3>
<p>Collection containing models. The most popular collection type describing JSON array of objects.</p>
<ul>
<li>Collection <strong>is</strong> serializable as <code>[ { ...user1 }, { ...user2 }, ... ]</code></li>
<li>All changes to enclosed model's attributes are treated as a change of the collection.</li>
</ul>
<pre><code class="highlight javascript"><span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">users</span> : Collection.of( User )
}
</code></pre>
<h2 id="serializable-model-references">Serializable model references</h2>
<p>Model attribute with reference to existing models or collections. Referenced objects <strong>will not</strong> be copied, destroyed, or validated as a part of the model.</p>
<p>References can be either deeply observable <strong>or</strong> serializable.</p>
<p>Serializable id-references is a Type-R way to describe many-to-one and many-to-many relashionship in JSON. Models must have an id to have serializable references. Serializable id-references are not observable.</p>
<p>Id references represented as model ids in JSON and appears as regular models at run time. Ids are being resolved to actual model instances with lookup in the base collection <strong>on first attribute access</strong>, which allows the definition of a complex serializable object graphs consisting of multiple collections of cross-referenced models fetched asynchronously.</p>
<h3 id="basecollection-parameter">baseCollection parameter</h3>
<p><code>baseCollection</code> argument could be:</p>
<ul>
<li>a direct reference to the singleton collection object</li>
<li>a function returning the collection which is called in a context of the model</li>
<li>a symbolic path, which is a string with a dot-separated path resolved relative to the model's <code>this</code>.</li>
</ul>
<h3 id="attribute-modelclass-memberof-basecollection-"><code>attribute</code> : ModelClass.memberOf( baseCollection )</h3>
<p>Model attribute holding serializable id-reference to a model from the base collection. Used to describe one-to-may relashioship with a model attribute represented in JSON as a model id.</p>
<ul>
<li>Attribute <strong>is</strong> serializable as <code>model.id</code></li>
<li>Changes of enclosed model's attributes <strong>will not</strong> trigger the change of the attribute.</li>
</ul>
<p>Attribute can be assigned with either the model from the base collection or the model id. If there are no such a model in the base collection <strong>on the moment of first attribute access</strong>, the attribute value will be <code>null</code>.</p>
<pre><code class="highlight javascript"><span class="hljs-keyword">static</span> attributes = {
<span class="hljs-comment">// Nested collection of users.</span>
users : Collection.of( User ),
<span class="hljs-comment">// Model from `users` serializable as `user.id`</span>
selectedUser : memberOf( <span class="hljs-string">'users'</span> )
}
</code></pre>
<h3 id="attribute-collection-subsetof-basecollection-"><code>attribute</code> : Collection.subsetOf( baseCollection )</h3>
<p>Collection of id-references to models from base collection. Used to describe many-to-many relationship with a collection of models represented in JSON as an array of model ids. The subset collection itself <strong>will be</strong> be copied, destroyed, and validated as a part of the owner model, but not the models in it.</p>
<ul>
<li>Collection <strong>is</strong> serializable as <code>[ user1.id, user2.id, ... ]</code>.</li>
<li>Changes of enclosed model's attributes <strong>will not</strong> trigger change of the collection.</li>
</ul>
<p>If some models are missing in the base collection <strong>on the moment of first attribute access</strong>, such a models will be removed from a subset collection.</p>
<pre><code class="highlight javascript"><span class="hljs-keyword">static</span> attributes = {
<span class="hljs-comment">// Nested collection of users.</span>
users : Collection.of( User ),
<span class="hljs-comment">// Collection with a subset of `users` serializable as an array of `user.id`</span>
selectedUsers : Collection.subsetOf( <span class="hljs-string">'users'</span> ) <span class="hljs-comment">// 'users' == function(){ return this.users }</span>
}
</code></pre>
<h2 id="observable-references">Observable references</h2>
<p>Non-serializable run time reference to models or collections. Used to describe a temporary observable application state.</p>
<h3 id="attribute-refto-modelorcollection-"><code>attribute</code> : refTo( ModelOrCollection )</h3>
<p>Model attribute holding a reference to a model or collection.</p>
<ul>
<li>Attribute <strong>is not</strong> serializable.</li>
<li>Changes of enclosed model's attributes <strong>will</strong> trigger change of the model.</li>
</ul>
<pre><code class="highlight javascript"><span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">users</span> : refTo( Collection.of( User ) ),
<span class="hljs-attr">selectedUser</span> : refTo( User )
}
</code></pre>
<h3 id="attribute-collection-ofrefsto-user-"><code>attribute</code> : Collection.ofRefsTo( User )</h3>
<p>Collection of references to models. The collection itself <strong>will be</strong> be copied, destroyed, and validated as a part of the model, but not the models in it.</p>
<ul>
<li>Collection <strong>is not</strong> serializable.</li>
<li>Changes of enclosed model's attributes <strong>will</strong> trigger change of the collection.</li>
</ul>
<pre><code class="highlight javascript"><span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">users</span> : Collection.of( User ),
<span class="hljs-attr">selectedUsers</span> : Collection.ofRefsTo( User )
}
</code></pre>
<h2 id="attribute-metadata">Attribute metadata</h2>
<h3 id="attribute-type-type-"><code>attribute</code> : type(Type)</h3>
<p>Convert attribute type to a <em>metatype</em>, which is a combination of type and metadata. Attribute's default value is the common example of the a metadata. Metadata can control all aspects of attribute behavior and. It's is added through a chain of calls after the <code>type( Type )</code> call.</p>
<p>Following values can be used as a <code>Type</code>:</p>
<ul>
<li>Constructor functions.</li>
<li>Plain object, meaning the nested model with a given attributes.</li>
<li>Array with either constructor or plain object inside, meaning the collection of models.</li>
</ul>
<pre><code class="highlight javascript"><span class="hljs-keyword">import</span> { define, type, Model }
<span class="hljs-keyword">const</span> Role = attributes({
<span class="hljs-attr">name</span> : <span class="hljs-built_in">String</span>
})
<span class="hljs-keyword">const</span> User = attributes({
<span class="hljs-attr">name</span> : type( <span class="hljs-built_in">String</span> ).value( <span class="hljs-string">'change me'</span> ),
<span class="hljs-attr">createdAt</span> : type( <span class="hljs-built_in">Date</span> ).dontSave
permissions : type({
<span class="hljs-attr">canDoA</span> : <span class="hljs-literal">true</span>,
<span class="hljs-attr">canDoB</span> : <span class="hljs-literal">true</span>
}),
<span class="hljs-attr">roles</span> : type( [Role] ) <span class="hljs-comment">// type( Collection.of( Role ) )</span>
flags : type( [{ <span class="hljs-comment">// type( Collection.of( attributes({ name : String }) ) )</span>
name : <span class="hljs-built_in">String</span>
}])
});
</code></pre>
<p>Since the attribute definition is a regular JavaScript, attribute metatype definition can be shared and reused across the different models and projects.</p>
<pre><code class="highlight javascript"><span class="hljs-keyword">import</span> { define, type, Model }
<span class="hljs-keyword">const</span> AString = type( <span class="hljs-built_in">String</span> ).value( <span class="hljs-string">"a"</span> );
<span class="hljs-keyword">const</span> Dummy = attributes({
<span class="hljs-attr">a</span> : AString,
<span class="hljs-attr">b</span> : type( <span class="hljs-built_in">String</span> ).value( <span class="hljs-string">"b"</span> )
});
</code></pre>
<h3 id="attribute-type-constructor-value-defaultvalue-"><code>attribute</code> : type(Constructor).value(defaultValue)</h3>
<p>Declare an attribute with type Constructor having the custom <code>defaultValue</code>. Normally, all attributes are initialized with a default constructor call.</p>
<pre><code class="highlight javascript">@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Person</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">phone</span> : type( <span class="hljs-built_in">String</span> ).value( <span class="hljs-literal">null</span> ) <span class="hljs-comment">// String attribute which is null by default.</span>
...
}
}
</code></pre>
<h3 id="attribute-type-constructor-null"><code>attribute</code> : type(Constructor).null</h3>
<p>Shorthand for <code>type(Constructor).value( null )</code>.</p>
<pre><code class="highlight javascript">@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Person</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">phone</span> : type( <span class="hljs-built_in">String</span> ).null <span class="hljs-comment">// String attribute which is null by default.</span>
...
}
}
</code></pre>
<h3 id="attribute-value-defaultvalue-"><code>attribute</code> : value( defaultValue )</h3>
<p>Similar to <code>type( T ).value( x )</code>, but infers the type from the default value. So, for instance, <code>type( String )</code> is equivalent to <code>value("")</code>.</p>
<pre><code class="highlight javascript"><span class="hljs-keyword">import</span> { define, type, Model }
@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Dummy</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">a</span> : value( <span class="hljs-string">"a"</span> )
}
}
</code></pre>
<h3 id="metatype-type-type-check-predicate-errormsg-"><code>metatype</code> type( Type ).check( predicate, errorMsg? )</h3>
<p>Attribute validation check.</p>
<ul>
<li><code>predicate : value => boolean</code> is the function taking attribute's value and returning <code>true</code> whenever the value is valid.</li>
<li>optional <code>errorMsg</code> is the error message which will be passed in case if the validation fail.</li>
</ul>
<p>If <code>errorMsg</code> is omitted, error message will be taken from <code>predicate.error</code>. It makes possible to define reusable validation functions.</p>
<pre><code class="highlight javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isAge</span>(<span class="hljs-params"> years </span>)</span>{
<span class="hljs-keyword">return</span> years >= <span class="hljs-number">0</span> && years < <span class="hljs-number">200</span>;
}
isAge.error = <span class="hljs-string">"Age must be between 0 and 200"</span>;
</code></pre>
<p>Attribute may have any number of checks attached which are being executed in a sequence. Validation stops when first check in sequence fails.
It can be used to define reusable attribute types as demonstrated below:</p>
<pre><code class="highlight javascript"><span class="hljs-comment">// Define new attribute metatypes encapsulating validation checks.</span>
<span class="hljs-keyword">const</span> Age = type( <span class="hljs-built_in">Number</span> )
.check( <span class="hljs-function"><span class="hljs-params">x</span> =></span> x == <span class="hljs-literal">null</span> || x >= <span class="hljs-number">0</span>, <span class="hljs-string">'I guess you are a bit older'</span> )
.check( <span class="hljs-function"><span class="hljs-params">x</span> =></span> x == <span class="hljs-literal">null</span> || x < <span class="hljs-number">200</span>, <span class="hljs-string">'No way man can be that old'</span> );
<span class="hljs-keyword">const</span> Word = type( <span class="hljs-built_in">String</span> ).check( <span class="hljs-function"><span class="hljs-params">x</span> =></span> indexOf( <span class="hljs-string">' '</span> ) < <span class="hljs-number">0</span>, <span class="hljs-string">'No spaces allowed'</span> );
@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Person</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">firstName</span> : Word,
<span class="hljs-attr">lastName</span> : Word,
<span class="hljs-attr">age</span> : Age
}
}
</code></pre>
<h3 id="metatype-type-type-required"><code>metatype</code> type( Type ).required</h3>
<p>Attribute validator checking that the attribute is not empty. Attribute value must be truthy to pass, <code>"Required"</code> string is used as validation error.</p>
<p><code>required</code> validator always checked first, no matter in which order validators were attached.</p>
<h3 id="attribute-type-type-get-value-attr-value-"><code>attribute</code> : type(Type).get( ( value, attr ) => value )</h3>
<p>Transform the value right <em>before it will be read</em>. The hook is executed in the context of the model.</p>
<h3 id="attribute-type-type-set-value-attr-value-"><code>attribute</code> : type(Type).set( ( value, attr ) => value )</h3>
<p>Transform the value right <em>before it will be assigned</em>. The hook is executed in the context of the model.</p>
<p>If set hook will return <code>undefined</code>, the attribute won't be assigned.</p>
<h3 id="metatype-type-type-dontsave"><code>metatype</code> type( Type ).dontSave</h3>
<p>Do <em>not</em> serialize the attribute.</p>
<h3 id="metatype-type-type-tojson-value-name-options-json-"><code>metatype</code> type( Type ).toJSON( ( value, name, options ) => json )</h3>
<p>Override the way how the attribute transforms to JSON. Attribute is not serialized when the function return <code>undefined</code>.</p>
<h3 id="metatype-type-type-parse-json-name-value-"><code>metatype</code> type( Type ).parse( ( json, name ) => value )</h3>
<p>When restoring attribute from JSON, transform the value before it will be assigned.</p>
<pre><code class="highlight javascript"><span class="hljs-comment">// Define custom boolean attribute type which is serialized as 0 or 1.</span>
<span class="hljs-keyword">const</span> MyWeirdBool = type( <span class="hljs-built_in">Boolean</span> )
.parse( <span class="hljs-function"><span class="hljs-params">x</span> =></span> x === <span class="hljs-number">1</span> )
.toJSON( <span class="hljs-function"><span class="hljs-params">x</span> =></span> x ? <span class="hljs-number">1</span> : <span class="hljs-number">0</span> );
</code></pre>
<h3 id="metatype-type-type-watcher-watcher-"><code>metatype</code> type( Type ).watcher( watcher )</h3>
<p>Attach custom reaction on attribute change. <code>watcher</code> can either be the model's method name or the function <code>( newValue, attr ) => void</code>. Watcher is executed in the context of the model.</p>
<pre><code class="highlight javascript">@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">User</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">name</span> : type( <span class="hljs-built_in">String</span> ).watcher( <span class="hljs-string">'onNameChange'</span> ),
<span class="hljs-attr">isAdmin</span> : <span class="hljs-built_in">Boolean</span>,
}
onNameChange(){
<span class="hljs-comment">// Cruel. But we need it for the purpose of the example.</span>
<span class="hljs-keyword">this</span>.isAdmin = <span class="hljs-keyword">this</span>.name.indexOf( <span class="hljs-string">'Admin'</span> ) >= <span class="hljs-number">0</span>;
}
}
</code></pre>
<h3 id="metatype-type-modelorcollection-changeevents-false-"><code>metatype</code> type( ModelOrCollection ).changeEvents( false )</h3>
<p>Turn off observable changes for the attribute.</p>
<p>Model automatically listens to change events of all nested models and collections triggering appropriate change events for its attributes. This declaration turns it off for the specific attribute.</p>
<h3 id="metatype-type-type-events-eventname-handler-"><code>metatype</code> type( Type ).events({ eventName : handler, ... })</h3>
<p>Automatically manage custom event subscription for the attribute. <code>handler</code> is either the method name or the handler function. <code>Type</code> needs to be a <code>Messenger</code> subclass from <code>@type-r/mixture</code> or include it as a mixin.</p>
<p>Both <code>Model</code> and <code>Collection</code> includes <code>Messenger</code> as a mixin.</p>
<h3 id="metatype-type-type-endpoint-endpoint-"><code>metatype</code> type( Type ).endpoint( <code>endpoint</code> )</h3>
<p>Override or define an I/O endpoint for the specific model's attribute.</p>
<h2 id="custom-attribute-metatypes">Custom attribute metatypes</h2>
<p>"Attribute metatype" is the Type-R attribute type descriptor with metadata attached.
Metatype is created by assigning the result of <code>type( T )</code> expression to some variable.</p>
<p>The following attribute types are available from <code>@type-r/ext-types</code> package.</p>
<h3 id="attribute-microsoftdate"><code>attribute</code> : MicrosoftDate</h3>
<p><code>Date</code> attribute represented in JSON as Microsoft date (represented in JSON as string <code>/Date(timestamp)</code>)</p>
<h3 id="attribute-timestamp"><code>attribute</code> : Timestamp</h3>
<p><code>Date</code> attribute represented in JSON as UNIX timestamp (the result of <code>date.getTime()</code>).</p>
<h3 id="attribute-integer"><code>attribute</code> : Integer</h3>
<p><code>Number</code> attribute converting value to integer on assignment. Can be called as function.</p>
<h3 id="attribute-email"><code>attribute</code> : Email</h3>
<p><code>String</code> attribute with email validation check.</p>
<h3 id="attribute-ipaddress"><code>attribute</code> : IPAddress</h3>
<p><code>String</code> attribute with IP address validation check.</p>
<h3 id="attribute-url"><code>attribute</code> : Url</h3>
<p><code>String</code> attribute with URL validation check.</p>
<h1 id="model-api">Model API</h1>
<h2 id="create-and-dispose">Create and dispose</h2>
<h3 id="new-model-attrs-options-">new Model( attrs?, options?)</h3>
<p>Create the model. If no <code>attrs</code> is supplied, initialize it with defaults taken from the attributes definition.</p>
<p>When no default value is explicitly provided for an attribute, it's initialized as <code>new AttributeType()</code> (just <code>AttributeType()</code> for primitives). When the default value is provided and it's not compatible with the attribute type, the value is converted to the proper type with <code>new Type( defaultValue )</code> call.</p>
<p>If <code>{parse: true}</code> option is set the <code>attrs</code> is assumed to be the JSON. In this case, <code>model.parse( attr )</code> and attribute's <code>parse</code> hooks will be called to give you an option to transform the JSON.</p>
<pre><code class="highlight javascript">@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Book</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">title</span> : <span class="hljs-string">''</span>,
<span class="hljs-attr">author</span> : <span class="hljs-string">''</span>
}
}
<span class="hljs-keyword">const</span> book = <span class="hljs-keyword">new</span> Book({
<span class="hljs-attr">title</span>: <span class="hljs-string">"One Thousand and One Nights"</span>,
<span class="hljs-attr">author</span>: <span class="hljs-string">"Scheherazade"</span>
});
</code></pre>
<h3 id="modelclass-from-attrs-options-">ModelClass.from(attrs, options?)</h3>
<p>Create <code>RecordClass</code> from attributes. Similar to direct model creation, but supports additional option for strict data validation.
If <code>{ strict : true }</code> option is passed the model validation will be performed immediately and an exception will be thrown in case of an error.</p>
<p>Type-R always perform type checks on assignments, convert types, and reject improper updates reporting it as error. It won't, however, execute custom validation
rules on every updates as validation is evaluated lazily. <code>strict</code> option will invoke custom validators and will throw on every error or warning instead of reporting them and continue.</p>
<pre><code class="highlight javascript"><span class="hljs-comment">// Fetch model with a given id.</span>
<span class="hljs-keyword">const</span> book = <span class="hljs-keyword">await</span> Book.from({ <span class="hljs-attr">id</span> : <span class="hljs-number">5</span> }).fetch();
<span class="hljs-comment">// Validate the body of an incoming HTTP request.</span>
<span class="hljs-comment">// Throw an exception if validation fails.</span>
<span class="hljs-keyword">const</span> body = MyRequestBody.from( ctx.request.body, { <span class="hljs-attr">parse</span> : <span class="hljs-literal">true</span>, <span class="hljs-attr">strict</span> : <span class="hljs-literal">true</span> });
</code></pre>
<h3 id="static-modelclass-create-attrs-options-"><code>static</code> ModelClass.create( attrs, options )</h3>
<p>Static factory function used internally by Type-R to create instances of the model.</p>
<p>May be redefined in the abstract Model base class to make it serializable type.</p>
<pre><code class="highlight javascript">@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Widget</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">type</span> : <span class="hljs-built_in">String</span>
}
<span class="hljs-keyword">static</span> create( attrs, options ){
<span class="hljs-keyword">switch</span>( attrs.type ){
<span class="hljs-keyword">case</span> <span class="hljs-string">"typeA"</span> : <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> TypeA( attrs, options );
<span class="hljs-keyword">case</span> <span class="hljs-string">"typeB"</span> : <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> TypeB( attrs, options );
}
}
}
@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TypeA</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Widget</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">type</span> : <span class="hljs-string">"typeA"</span>,
...
}
}
@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TypeB</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Widget</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">type</span> : <span class="hljs-string">"typeB"</span>,
...
}
}
</code></pre>
<h3 id="model-clone-">model.clone()</h3>
<p>Create the deep copy of the aggregation tree, recursively cloning all aggregated models and collections. References to shared members will be copied, but not shared members themselves.</p>
<h3 id="callback-model-initialize-attrs-options-"><code>callback</code> model.initialize(attrs?, options?)</h3>
<p>Called at the end of the <code>Model</code> constructor when all attributes are assigned and the model's inner state is properly initialized. Takes the same arguments as
a constructor.</p>
<h3 id="model-dispose-">model.dispose()</h3>
<p>Recursively dispose the model and its aggregated members. "Dispose" means that elements of the aggregation tree will unsubscribe from all event sources. It's crucial to prevent memory leaks in SPA.</p>
<p>The whole aggregation tree will be recursively disposed, shared members won't.</p>
<h2 id="read-and-update">Read and update</h2>
<h3 id="model-cid">model.cid</h3>
<p>Read-only client-side model's identifier. Generated upon creation of the model and is unique for every model's instance. Cloned models will have different <code>cid</code>.</p>
<h3 id="model-id">model.id</h3>
<p>Predefined model's attribute, the <code>id</code> is an arbitrary string (integer id or UUID). <code>id</code> is typically generated by the server. It is used in JSON for id-references.</p>
<p>Records can be retrieved by <code>id</code> from collections, and there can be just one instance of the model with the same <code>id</code> in the particular collection.</p>
<h3 id="model-attrname">model.attrName</h3>
<p>Model's attributes can be directly accessed with their names as a regular class properties.</p>
<p>If the value is not compatible with attribute's type from the declaration on assignment, it is converted with <code>Type( value )</code> call for primitive types, and with <code>new Type( value )</code> for other types.</p>
<p>There is an important exception in type convertion logic for models and collections. Instead of applying a contructor, Type-R will try to update existing model and collection instances in place calling their <code>set()</code> method instead. This logic keeps the model and collection references stable and safe to pass around.</p>
<p>Model triggers events on changes:</p>
<ul>
<li><code>change:attrName</code> <em>( model, value )</em>.</li>
<li><code>change</code> <em>( model )</em>.</li>
</ul>
<aside class="warning">Please note, that you *have to declare all attributes* in `static attributes` declaration.</aside>
<pre><code class="highlight javascript">@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Book</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Model</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">title</span> : <span class="hljs-built_in">String</span>,
<span class="hljs-attr">author</span> : <span class="hljs-built_in">String</span>
price : <span class="hljs-built_in">Number</span>,
<span class="hljs-attr">publishedAt</span> : <span class="hljs-built_in">Date</span>,
<span class="hljs-attr">available</span> : <span class="hljs-built_in">Boolean</span>
}
}
<span class="hljs-keyword">const</span> myBook = <span class="hljs-keyword">new</span> Book({ <span class="hljs-attr">title</span> : <span class="hljs-string">"State management with Type-R"</span> });
myBook.author = <span class="hljs-string">'Vlad'</span>; <span class="hljs-comment">// That works.</span>
myBook.price = <span class="hljs-string">'Too much'</span>; <span class="hljs-comment">// Converted with Number( 'Too much' ), resulting in NaN.</span>
myBook.price = <span class="hljs-string">'123'</span>; <span class="hljs-comment">// = Number( '123' ).</span>
myBook.publishedAt = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(); <span class="hljs-comment">// Type is compatible, no conversion.</span>
myBook.publishedAt = <span class="hljs-string">'1678-10-15 12:00'</span>; <span class="hljs-comment">// new Date( '1678-10-15 12:00' )</span>
myBook.available = some && weird || condition; <span class="hljs-comment">// Will always be Boolean. Or null.</span>
</code></pre>