forked from gaperton/Type-R
-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathindex.html
More file actions
2359 lines (2167 loc) · 178 KB
/
index.html
File metadata and controls
2359 lines (2167 loc) · 178 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 3.0 API Reference</title>
<link rel="icon" href="images/logo-dark.png" />
<link href="lib/stylesheets/screen.css" rel="stylesheet" type="text/css" media="screen" />
<link href="lib/stylesheets/print.css" rel="stylesheet" type="text/css" media="print" />
<link href="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="lib/javascripts/all.js" type="text/javascript"></script>
<script>
$(function() {
var langs = [];
langs.push("javascript");
langs.push("typescript");
setupLanguages( langs );
});
</script>
</head>
<body class="index">
<a href="#" id="nav-button">
<span>
NAV
<img src="images/navbar.png" />
</span>
</a>
<div class="tocify-wrapper">
<div class="logo-section">
<img src="images/logo.png" />
<div class="logo-text">
<div class="logo-caption">Type-R 3.0</div>
<div>universal state management</div>
</div>
</div>
<div class="lang-selector">
<a href="#" data-language-name="javascript">javascript</a>
<a href="#" data-language-name="typescript">typescript</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>
<li><a href="http://www.volicon.com/">Supported by <img style="vertical-align: middle" src="images/volicon_verizon_dm.png"/></a></li>
</ul>
</div>
<div class="page-wrapper">
<div class="content">
<h1 id="getting-started">Getting started</h1>
<h2 id="overview">Overview</h2>
<p>Type-R is the TypeScript and JavaScript model framework helping to define and manage the complex application state as a combination of reusable parts. Type-R cover the needs of business logic and data layers in 3-tier application architecture, providing the presentation layer with the unified technique to handle the UI and domain state. Type-R data structures look and feel (and, in some aspects, behaves) more like classes in the statically typed languages.</p>
<p>Type-R in unopinionated on the way how an application state should be managed ("single source of truth" or "distributed state"). It can support all approaches equally well being not dependent on singletons and having powerful capabilities for state synchronization.</p>
<p><img src="images/3-layer-client.png" alt="overview"></p>
<p>A state is defined as a superposition of typed records and collections. A record is a class with a known set of attributes of predefined types possibly holding other records and collections in its attributes, describing the data structure of arbitrary complexity. Record with its attributes forms an aggregation tree with deeply observable attributes changes. Attribute types are checked on assignments and invalid changes are being rejected, therefore it is guaranteed that the application state will preserve the valid shape.</p>
<p>Application state defined with Type-R is serializable to JSON by default. Aggregation tree of records and collections is mapped in JSON as a tree of plain objects and arrays. Normalized data represented as a set of collections of records cross-referencing each other are supported as first-class serialization scenario.</p>
<p>A record may have an associated IOEndpont representing the I/O protocol for CRUD and collection fetch operations which enables the persistence API for the particular record/collection class pair. Some useful endpoints (<code>restfulIO</code>, <code>localStorageIO</code>, etc) are provided by <code>type-r/endpoints/*</code> packages, and developers can define their own I/O endpoints implementing any particular persistence transport or API.</p>
<p>Record attributes may have custom validation rules attached to them. Validation is being triggered transparently on demand and its result is cached across the record/collection aggregation tree, making subsequent calls to the validation API extremely cheap.</p>
<p>All aspects of record behavior including serialization and validation can be controlled on attribute level with declarative definitions combining attribute types with metadata. Attribute definitions ("metatypes") can be reused across different models forming the domain-specific language of model declarations. Some useful attribute metatypes (<code>Email</code>, <code>Url</code>, <code>MicrosoftDate</code>, etc) are provided by <code>type-r/ext-types</code> package.</p>
<h2 id="how-type-r-compares-to-x-">How Type-R compares to X?</h2>
<p>Type-R (former "NestedTypes") project was started in 2014 in Volicon as a modern successor to BackboneJS models, which would match Ember Data in its capabilities to work with a complex state while retaining the BackboneJS simplicity, modularity, and some degree of backward API compatibility. It replaced BackboneJS in the model layer of Volicon products, and it became the key technology in Volicon's strategy to gradually move from BackboneJS Views to React in the view layer.</p>
<p><a href="https://guides.emberjs.com/v2.2.0/models/">Ember Data</a> is the closest thing to Type-R by its capabilities, with <a href="http://backbonejs.org/#Model">BackboneJS models and collections</a> being the closest thing by the API, and <a href="https://github.com/mobxjs/mobx">mobx</a> being pretty close in the way how the UI state is managed.</p>
<p>Type-R, however, takes a very different approach to all of them:</p>
<ul>
<li>Type-R models look and feel more like classes in a statically typed language with the majority of features being controlled by attribute metadata.</li>
<li>Type-R is built around the concept of <em>aggregation trees</em> formed by nested records and collections and it knows how to clone, serialize, and validate complex objects with cross-references properly.</li>
<li>In contrast to BackboneJS, Record is <em>not an object hash</em> but the class with statically typed and dynamically checked attributes.</li>
<li>In contrast to mobx, Type-R detects <em>deeply nested changes</em>.</li>
<li>In contrast to Ember Data, Type-R doesn't require the singleton global store. In Type-R, stores are a special kind of records and there might be as many dynamically created and disposed of stores as you need, starting with no stores at all.</li>
</ul>
<table>
<thead>
<tr>
<th>Feature</th>
<th>Type-R</th>
<th>Backbone Models</th>
<th>Ember Data</th>
<th>mobx</th>
</tr>
</thead>
<tbody>
<tr>
<td>Observable changes in object graph</td>
<td>✓</td>
<td>-</td>
<td>-</td>
<td>✓</td>
</tr>
<tr>
<td>JSON Serialization</td>
<td>✓</td>
<td>✓</td>
<td>✓</td>
<td>-</td>
</tr>
<tr>
<td>Validation</td>
<td>✓</td>
<td>✓</td>
<td>✓</td>
<td>-</td>
</tr>
<tr>
<td>Dynamic Type Safety</td>
<td>✓</td>
<td>-</td>
<td>for serialization only</td>
<td>-</td>
</tr>
<tr>
<td>Aggregation</td>
<td>✓</td>
<td>-</td>
<td>-</td>
<td>-</td>
</tr>
<tr>
<td>Relations by id</td>
<td>✓</td>
<td>-</td>
<td>✓</td>
<td>- </td>
</tr>
<tr>
<td>Generalized I/O</td>
<td>✓</td>
<td>sync function</td>
<td>✓</td>
<td>- </td>
</tr>
</tbody>
</table>
<h2 id="features-by-example">Features by example</h2>
<p>Here's the brief overview of features groped by application purpose.</p>
<h3 id="persistent-domain-state">Persistent domain state</h3>
<p>The basic building block is the <code>Record</code> class. To fetch data from the server, a developer creates the subclass of the <code>Record</code> describing its attribute types and attaches the <code>restfulIO</code> endpoint. It enables the persistence API allowing the developer to fetch the collection from the server. <code>restfulIO</code> expects the server to implement the standard RESTful API expected by BackboneJS models.</p>
<ul>
<li><code>GET /api/users</code> - fetch all the users</li>
<li><code>POST /api/users</code> - create the user</li>
<li><code>GET /api/users/:id</code> - fetch the user with a given id</li>
<li><code>PUT /api/users/:id</code> - update the user with a given id</li>
<li><code>DELETE /api/users/:id</code> - delete the user with a given id</li>
</ul>
<p>Record and collection are serializable to and can be parsed from JSON with no additional effort. A mapping to JSON can be customized for collections, records, and individual attributes. The Record validates all updates casting attribute values to declared attribute types to protect the state structure from the protocol incompatibilities and improper assignments.</p>
<pre><code class="highlight javascript">@define User extends Record {
<span class="hljs-keyword">static</span> endpoint = restfulIO( <span class="hljs-string">'/api/users'</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">email</span> : <span class="hljs-built_in">String</span>,
<span class="hljs-attr">createdAt</span> : <span class="hljs-built_in">Date</span>
}
}
<span class="hljs-keyword">const</span> users = <span class="hljs-keyword">new</span> User.Collection();
<span class="hljs-keyword">await</span> users.fetch();
expect( users.first().createdAt ).toBeInstanceOf( <span class="hljs-built_in">Date</span> );
expect( <span class="hljs-keyword">typeof</span> users.toJSON()[ <span class="hljs-number">0</span> ].createdAt ).toBe( <span class="hljs-string">"string"</span> );
</code></pre>
<pre><code class="highlight typescript"><span class="hljs-meta">@define</span> User <span class="hljs-keyword">extends</span> Record {
<span class="hljs-keyword">static</span> endpoint = restfulIO( <span class="hljs-string">'/api/users'</span> );
<span class="hljs-comment">// Type-R can infer attribute types from TypeScript type annotations.</span>
<span class="hljs-meta">@auto</span> name : <span class="hljs-built_in">string</span>
<span class="hljs-meta">@auto</span> email : <span class="hljs-built_in">string</span>
<span class="hljs-meta">@auto</span> createdAt : <span class="hljs-built_in">Date</span>
}
<span class="hljs-keyword">const</span> users : Collection<User> = <span class="hljs-keyword">new</span> User.Collection();
<span class="hljs-keyword">await</span> users.fetch();
expect( users.first().createdAt ).toBeInstanceOf( <span class="hljs-built_in">Date</span> );
expect( <span class="hljs-keyword">typeof</span> users.toJSON()[ <span class="hljs-number">0</span> ].createdAt ).toBe( <span class="hljs-string">"string"</span> );
</code></pre>
<h3 id="ui-state-and-observable-changes">UI state and observable changes</h3>
<p>Type-R provides the universal technique to working with the UI and domain state. To define the UI state, a developer creates the subclass of the <code>Record</code> with attributes holding all the necessary state data possibly along with the persistent data which can become the part of the same local UI state. The UI state itself can be a part of some particular view or UI component, it can be managed as a singleton ("single source of truth"), or both at the same time. Type-R is unopinionated on the application state structure leaving this decision to the developer.</p>
<p>Records and collections form an aggregation tree with deeply observable changes, so it's enough to subscribe to the single <code>change</code> event from the <code>UIState</code> to get updates on both data arrival and local changes of the state attributes. Records and collections can be indefinitely nested to describe a state of arbitrary complexity. The developer can attach reactions on changes to the records, their individual attributes, and collections. Additional changes made in reactions will be executed in the scope of the same "change transaction" and won't trigger additional change events.</p>
<pre><code class="highlight javascript">@define UIState extends Record {
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">users</span> : User.Collection,
<span class="hljs-attr">selectedUser</span> : memberOf( <span class="hljs-string">'users'</span> )
}
}
<span class="hljs-keyword">const</span> uiState = <span class="hljs-keyword">new</span> UIState();
uiState.on( <span class="hljs-string">'change'</span>, () => {
<span class="hljs-built_in">console</span>.log( <span class="hljs-string">'Something is changed'</span> );
updateUI();
});
uiState.users.fetch();
</code></pre>
<pre><code class="highlight typescript"><span class="hljs-meta">@define</span> UIState <span class="hljs-keyword">extends</span> Record {
<span class="hljs-comment">// For collections and more complex types attribute type must be provided explicitly</span>
<span class="hljs-meta">@type</span>( User.Collection ).as users : Collection<User>
<span class="hljs-meta">@memberOf</span>( <span class="hljs-string">'users'</span> ).as selectedUser : User
}
<span class="hljs-keyword">const</span> uiState = <span class="hljs-keyword">new</span> UIState();
uiState.on( <span class="hljs-string">'change'</span>, <span class="hljs-function"><span class="hljs-params">()</span> =></span> {
<span class="hljs-built_in">console</span>.log( <span class="hljs-string">'Something is changed'</span> );
updateUI();
});
uiState.users.fetch();
</code></pre>
<h3 id="validation">Validation</h3>
<p>Type-R supports validation as attribute-level checks attached to attribute definitions as metadata. Attribute type together with checks forms an "attribute metatype", which can be defined separately and reused across multiple record definitions.</p>
<p>Validation rules are evaluated recursively on the aggregation tree on first access to the validation API, and validations results are cached in records and collections across the tree till the next update. The validation is automatic, subsequent calls to the validation API are cheap, and the developer doesn't need to manually trigger the validation on data changes.</p>
<p>The majority of checks in a real application will be a part of attribute "metatypes", while the custom validation can be also defined on the <code>Record</code> and <code>Collection</code> level to check data integrity and cross-attributes dependencies.</p>
<pre><code class="highlight javascript"><span class="hljs-keyword">const</span> Email = type( <span class="hljs-built_in">String</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">"Doesn't look like an email"</span> );
@define User extends Record {
<span class="hljs-keyword">static</span> endpoint = restfulIO( <span class="hljs-string">'/api/users'</span> );
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">name</span> : type( <span class="hljs-built_in">String</span> ).required,
<span class="hljs-attr">email</span> : type( Email ).required,
<span class="hljs-attr">createdAt</span> : type( <span class="hljs-built_in">Date</span> ).check( <span class="hljs-function"><span class="hljs-params">x</span> =></span> x.getTime() <= <span class="hljs-built_in">Date</span>.now() )
}
}
<span class="hljs-keyword">const</span> users = <span class="hljs-keyword">new</span> User.Collection();
users.add({ <span class="hljs-attr">email</span> : <span class="hljs-string">'john'</span> });
expect( users.isValid() ).toBe( <span class="hljs-literal">false</span> );
expect( users.first().isValid() ).toBe( <span class="hljs-literal">false</span> );
users.first().name = <span class="hljs-string">"John"</span>;
users.first().email = <span class="hljs-string">"john@ny.com"</span>;
expect( users.isValid() ).toBe( <span class="hljs-literal">true</span> );
</code></pre>
<pre><code class="highlight typescript"><span class="hljs-keyword">const</span> Email = <span class="hljs-keyword">type</span>( <span class="hljs-built_in">String</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">"Doesn't look like an email"</span> );
<span class="hljs-meta">@define</span> User <span class="hljs-keyword">extends</span> Record {
<span class="hljs-keyword">static</span> endpoint = restfulIO( <span class="hljs-string">'/api/users'</span> );
<span class="hljs-comment">// @type(...).as converts Type-R attribute type definition to the TypeScript decorator.</span>
<span class="hljs-meta">@type</span>( <span class="hljs-built_in">String</span> ).required.as
name : <span class="hljs-built_in">string</span>
<span class="hljs-meta">@type</span>( Email ).required.as
email : <span class="hljs-built_in">string</span>
<span class="hljs-meta">@type</span>( <span class="hljs-built_in">Date</span> ).check( <span class="hljs-function"><span class="hljs-params">x</span> =></span> x.getTime() <= <span class="hljs-built_in">Date</span>.now() ).as
createdAt : <span class="hljs-built_in">Date</span>
}
<span class="hljs-keyword">const</span> users = <span class="hljs-keyword">new</span> User.Collection();
users.add({ email : <span class="hljs-string">'john'</span> });
expect( users.isValid() ).toBe( <span class="hljs-literal">false</span> );
expect( users.first().isValid() ).toBe( <span class="hljs-literal">false</span> );
users.first().name = <span class="hljs-string">"John"</span>;
users.first().email = <span class="hljs-string">"john@ny.com"</span>;
expect( users.isValid() ).toBe( <span class="hljs-literal">true</span> );
</code></pre>
<h2 id="installation-and-requirements">Installation and requirements</h2>
<p>Is packed as UMD and ES6 module. No peer dependencies are required.</p>
<p><code>npm install type-r --save-dev</code></p>
<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>
<h2 id="reactjs-bindings">ReactJS bindings</h2>
<p><a href="https://volijs.github.io/React-MVx/">React-MVx</a> is a glue framework which uses Type-R to manage the UI state in React and the <a href="https://github.com/VoliJs/NestedLink">NestedLink</a> library to implement two-way data binding. React-MVx provides the complete MVVM solution on top of ReactJS, featuring:</p>
<ul>
<li>Type-R <a href="https://volijs.github.io/Type-R/#record">Record</a> to manage the local <a href="https://volijs.github.io/React-MVx/#state">component's state</a>.</li>
<li><a href="https://volijs.github.io/React-MVx/#link">two-way data binding</a> for UI and domain state.</li>
<li>Hassle-free form validation (due to the combination of features of Type-R and NestedLink).</li>
<li><a href="https://volijs.github.io/Type-R/#definition">Type-R type annotation</a> used to define component <a href="https://volijs.github.io/React-MVx/#props">props</a> and <a href="https://volijs.github.io/React-MVx/#context">context</a>.</li>
</ul>
<h2 id="usage-with-nodejs">Usage with NodeJS</h2>
<p>Type-R can be used at the server side to build the business logic layer by defining the custom I/O endpoints to store data in a database. Type-R dynamic type safety features are particularly advantageous when schema-less JSON databases (like Couchbase) are being used.</p>
<p><img src="images/3-layer-server.png" alt="server"></p>
<h1 id="record">Record</h1>
<p>Record is an optionally persistent class having the predefined set of attributes. Each attribute is the property of known type which is protected from improper assigments at run-time, is serializable to JSON by default, has deeply observable changes, and may have custom validation rules attached.</p>
<p>Records may have other records and collections of records stored in its attributes describing an application state of an arbitrary complexity. These nested records and collections are considered to be an integral part of the parent record forming an <em>aggregation tree</em> which can be serialized to JSON, cloned, and disposed of as a whole.</p>
<p>All aspects of an attribute behavior are controlled with attribute metadata, which (taken together with its type) is called <em>attribite metatype</em>. Metatypes can be declared separately and reused across multiple records definitions.</p>
<pre><code class="highlight javascript"><span class="hljs-keyword">import</span> { define, type, Record } <span class="hljs-keyword">from</span> <span class="hljs-string">'type-r'</span>
<span class="hljs-comment">// ⤹ required to make magic work </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-comment">// ⤹ attribute's declaration</span>
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">firstName</span> : <span class="hljs-string">''</span>, <span class="hljs-comment">// ⟵ String type is inferred from the default value</span>
lastName : <span class="hljs-built_in">String</span>, <span class="hljs-comment">// ⟵ Or you can just mention its constructor</span>
email : type(<span class="hljs-built_in">String</span>).value(<span class="hljs-literal">null</span>), <span class="hljs-comment">//⟵ Or you can provide both</span>
createdAt : <span class="hljs-built_in">Date</span>, <span class="hljs-comment">// ⟵ And it works for any constructor.</span>
<span class="hljs-comment">// And you can attach ⤹ metadata to fine-tune attribute's behavior</span>
lastLogin : type(<span class="hljs-built_in">Date</span>).value(<span class="hljs-literal">null</span>).toJSON(<span class="hljs-literal">false</span>) <span class="hljs-comment">// ⟵ not serializable</span>
}
}
<span class="hljs-keyword">const</span> user = <span class="hljs-keyword">new</span> User();
<span class="hljs-built_in">console</span>.log( user.createdAt ); <span class="hljs-comment">// ⟵ this is an instance of Date created for you.</span>
<span class="hljs-keyword">const</span> users = <span class="hljs-keyword">new</span> User.Collection(); <span class="hljs-comment">// ⟵ Collections are defined automatically.</span>
users.on( <span class="hljs-string">'changes'</span>, () => updateUI( users ) ); <span class="hljs-comment">// ⟵ listen to the changes.</span>
users.set( json, { <span class="hljs-attr">parse</span> : <span class="hljs-literal">true</span> } ); <span class="hljs-comment">// ⟵ parse raw JSON from the server.</span>
users.updateEach( <span class="hljs-function"><span class="hljs-params">user</span> =></span> user.firstName = <span class="hljs-string">''</span> ); <span class="hljs-comment">// ⟵ bulk update triggering 'changes' once</span>
</code></pre>
<pre><code class="highlight typescript"><span class="hljs-keyword">import</span> { define, attr, <span class="hljs-keyword">type</span>, Record } <span class="hljs-keyword">from</span> <span class="hljs-string">'type-r'</span>
<span class="hljs-keyword">import</span> <span class="hljs-string">"reflect-metadata"</span> <span class="hljs-comment">// Required for @auto without arguments</span>
<span class="hljs-comment">// ⤹ required to make the magic work </span>
<span class="hljs-meta">@define</span> <span class="hljs-keyword">class</span> User <span class="hljs-keyword">extends</span> Record {
<span class="hljs-comment">// ⤹ attribute's declaration</span>
<span class="hljs-comment">// IMPORTANT: attributes will be initialized even if no default value is provided.</span>
<span class="hljs-meta">@auto</span> lastName : <span class="hljs-built_in">string</span> <span class="hljs-comment">// ⟵ @auto decorator extracts type from the Reflect metadata</span>
<span class="hljs-meta">@auto</span> createdAt : <span class="hljs-built_in">Date</span> <span class="hljs-comment">// ⟵ It works for any constructor.</span>
<span class="hljs-meta">@auto</span>(<span class="hljs-string">'somestring'</span>) firstName : <span class="hljs-built_in">string</span> <span class="hljs-comment">//⟵ The custom default value must be passed to @auto decorator.</span>
<span class="hljs-meta">@auto</span>(<span class="hljs-literal">null</span>) updatedAt : <span class="hljs-built_in">Date</span>
<span class="hljs-comment">// You have to pass the type explicitly if reflect-metadata is not used.</span>
<span class="hljs-meta">@type</span>(<span class="hljs-built_in">String</span>).as email : <span class="hljs-built_in">string</span>
<span class="hljs-comment">// Or, you can tell Type-R to infer type from the default value.</span>
<span class="hljs-meta">@value</span>(<span class="hljs-string">''</span>).as email2 : <span class="hljs-built_in">string</span>
<span class="hljs-comment">// Type cannot be inferred from null default values, and needs to be specified explicitly</span>
<span class="hljs-meta">@type</span>(<span class="hljs-built_in">String</span>).value(<span class="hljs-literal">null</span>).as email3 : <span class="hljs-built_in">string</span>
<span class="hljs-comment">// You can attach ⤹ metadata to fine-tune attribute's behavior</span>
<span class="hljs-meta">@type</span>(<span class="hljs-built_in">Date</span>).toJSON(<span class="hljs-literal">false</span>).as
lastLogin : <span class="hljs-built_in">Date</span><span class="hljs-comment">// ⟵ not serializable</span>
}
<span class="hljs-keyword">const</span> user = <span class="hljs-keyword">new</span> User();
<span class="hljs-built_in">console</span>.log(user.createdAt); <span class="hljs-comment">// ⟵ this is an instance of Date created for you.</span>
<span class="hljs-keyword">const</span> users : Collection<User> = <span class="hljs-keyword">new</span> User.Collection(); <span class="hljs-comment">// ⟵ Collections are defined automatically.</span>
users.on(<span class="hljs-string">'changes'</span>, <span class="hljs-function"><span class="hljs-params">()</span> =></span> updateUI(users)); <span class="hljs-comment">// ⟵ listen to the changes.</span>
users.set(json, { parse : <span class="hljs-literal">true</span> }); <span class="hljs-comment">// ⟵ parse raw JSON from the server.</span>
users.updateEach( <span class="hljs-function"><span class="hljs-params">user</span> =></span> user.firstName = <span class="hljs-string">''</span> ); <span class="hljs-comment">// ⟵ bulk update triggering 'changes' once</span>
</code></pre>
<h2 id="definition">Definition</h2>
<p>Record definition is ES6 class extending <code>Record</code> preceeded by <code>@define</code> class decorator. </p>
<p>Unlike in the majority of the JS state management framework, Record is <b>not the key-value hash</b>. Record has typed attributes with metadata controlling different aspects of attribute beavior. Therefore, developer needs to create the Record subclass to describe the data structure of specific shape, in a similar way as it's done in statically typed languages. The combination of an attribute type and metadata is called <em>metatype</em> and can be reused across record definitions.</p>
<p>The minimal record definition looks like this:</p>
<pre><code class="highlight javascript">@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyRecord</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>
}
}
</code></pre>
<pre><code class="highlight typescript"><span class="hljs-meta">@define</span> <span class="hljs-keyword">class</span> MyRecord <span class="hljs-keyword">extends</span> Record {
<span class="hljs-meta">@auto</span> name : <span class="hljs-built_in">string</span>
}
</code></pre>
<h3 id="static-attributes-name-attrdef-"><code>static</code> attributes = { name : <code>attrDef</code>, ... }</h3>
<p>Record's attributes definition. Lists attribute names along with their types, default values, and metadata controlling different aspects of attribute behavior.</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">Record</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">name</span> : type( <span class="hljs-built_in">String</span> ).value( <span class="hljs-string">'John Dow'</span> ),
<span class="hljs-attr">email</span> : <span class="hljs-string">'john.dow@mail.com'</span>, <span class="hljs-comment">// Same as type( String ).value( 'john.dow@mail.com' )</span>
address : <span class="hljs-built_in">String</span>, <span class="hljs-comment">// Same as type( String ).value( '' )</span>
}
}
</code></pre>
<pre><code class="highlight typescript"><span class="hljs-comment">// You should not use `static attributes` in TypeScript. Use decorators instead.</span>
<span class="hljs-meta">@define</span> <span class="hljs-keyword">class</span> User <span class="hljs-keyword">extends</span> Record {
<span class="hljs-comment">// Complete form of an attribute definition.</span>
<span class="hljs-meta">@type</span>( <span class="hljs-built_in">String</span> ).value( <span class="hljs-string">'John Dow'</span> ).as name : <span class="hljs-built_in">string</span>,
<span class="hljs-comment">// Attribute type is inferred from the default value.</span>
<span class="hljs-meta">@value</span>( <span class="hljs-string">'john.dow@mail.com'</span> ).as email : <span class="hljs-built_in">string</span> , <span class="hljs-comment">// Same as @type( String ).value( 'john.dow@mail.com' ).as</span>
<span class="hljs-comment">// Attribute type is inferred from the TypeScript type declaration.</span>
<span class="hljs-meta">@auto</span> address : <span class="hljs-built_in">string</span>, <span class="hljs-comment">// Same as @type( String ).value( '' )</span>
<span class="hljs-comment">// Same as above, but with a custom default value.</span>
<span class="hljs-meta">@auto</span>( <span class="hljs-string">'john.dow@mail.com'</span> ) email2 : <span class="hljs-built_in">string</span> <span class="hljs-comment">// Same as @value( 'john.dow@mail.com' ).as</span>
}
</code></pre>
<p>The Record guarantee that <em>every attribute will retain the value of the declared type</em>. Whenever an attribute is being assigned with the value which is not compatible with its declared type, the type is being converted with an invocation of the constructor: <code>new Type( value )</code> (primitive types are treated specially).</p>
<h3 id="static-idattribute-attrname-"><code>static</code> idAttribute = 'attrName'</h3>
<p>A record'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 Record's <code>idAttribute</code> to transparently map from that key to id.</p>
<p>Record's <code>id</code> property will still be linked to Record'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">Record</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>
<pre><code class="highlight typescript"><span class="hljs-meta">@define</span> <span class="hljs-keyword">class</span> Meal <span class="hljs-keyword">extends</span> Record {
<span class="hljs-keyword">static</span> idAttribute = <span class="hljs-string">"_id"</span>;
<span class="hljs-meta">@auto</span> _id : <span class="hljs-built_in">number</span>
<span class="hljs-meta">@auto</span> name : <span class="hljs-built_in">string</span>
}
<span class="hljs-keyword">const</span> cake = <span class="hljs-keyword">new</span> Meal({ _id: <span class="hljs-number">1</span>, name: <span class="hljs-string">"Cake"</span> });
alert(<span class="hljs-string">"Cake id: "</span> + cake.id);
</code></pre>
<h3 id="attrdef-constructor"><code>attrDef</code> : Constructor</h3>
<p>Constructor function is the simplest form of attribute definition. Any constructor function which behaves as <em>converting constructor</em> (like <code>new Date( msecs )</code>) may be used as an attribute type.</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">Record</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">// String attribute which is "" by default.</span>
createdAt : <span class="hljs-built_in">Date</span>, <span class="hljs-comment">// Date attribute</span>
...
}
}
</code></pre>
<pre><code class="highlight typescript"><span class="hljs-comment">// In typescript, @auto decorator will extract constructor function from the TypeScript type</span>
<span class="hljs-meta">@define</span> <span class="hljs-keyword">class</span> Person <span class="hljs-keyword">extends</span> Record {
<span class="hljs-meta">@auto</span> name : <span class="hljs-built_in">string</span> <span class="hljs-comment">// String attribute which is "" by default.</span>
<span class="hljs-meta">@auto</span> createdAt : <span class="hljs-built_in">Date</span> <span class="hljs-comment">// Date attribute</span>
<span class="hljs-comment">// Or, it can be specified explicitly with @type decorator.</span>
<span class="hljs-meta">@type</span>( <span class="hljs-built_in">Date</span> ).as updatedAt : <span class="hljs-built_in">Date</span> <span class="hljs-comment">// Date attribute</span>
...
}
</code></pre>
<h3 id="attrdef-defaultvalue"><code>attrDef</code> : defaultValue</h3>
<p>Any non-function value used as attribute definition is treated as an attribute's default value. Attribute's type is being inferred from the value.</p>
<p>Type cannot be properly inferred from the <code>null</code> values and functions.
Use the general form of attribute definition in such cases: <code>value( theFunction )</code>, <code>type( Boolean ).value( null )</code>.</p>
<pre><code class="highlight javascript">@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">GridColumn</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-comment">// String attribute which is '' by default.</span>
render : value( <span class="hljs-function"><span class="hljs-params">x</span> =></span> x ), <span class="hljs-comment">// Infer Function type from the default value.</span>
...
}
}
</code></pre>
<pre><code class="highlight typescript"><span class="hljs-comment">// In typescript, @value decorator will extract constructor function from the default value.</span>
<span class="hljs-meta">@define</span> <span class="hljs-keyword">class</span> GridColumn <span class="hljs-keyword">extends</span> Record {
<span class="hljs-meta">@value</span>( <span class="hljs-string">''</span> ).as name : <span class="hljs-built_in">string</span> <span class="hljs-comment">// String attribute which is '' by default.</span>
<span class="hljs-meta">@value</span>( <span class="hljs-function"><span class="hljs-params">x</span> =></span> x ).as render : <span class="hljs-built_in">Function</span>
...
}
</code></pre>
<h3 id="attrdef-type-constructor-value-defaultvalue-"><code>attrDef</code> : type(Constructor).value(defaultValue)</h3>
<p>Declare an attribute with type T having the custom <code>defaultValue</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">Record</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>
<pre><code class="highlight typescript"><span class="hljs-meta">@define</span> <span class="hljs-keyword">class</span> Person <span class="hljs-keyword">extends</span> Record {
<span class="hljs-meta">@type</span>( <span class="hljs-built_in">String</span> ).value( <span class="hljs-literal">null</span> ).as phone : <span class="hljs-built_in">string</span> <span class="hljs-comment">// String attribute which is null by default.</span>
<span class="hljs-comment">// There's an easy way of doing that in TypeScript.</span>
<span class="hljs-meta">@auto</span>( <span class="hljs-literal">null</span> ).as phone : <span class="hljs-built_in">string</span>
...
}
</code></pre>
<p>If record needs to reference itself in its attributes definition, <code>@predefine</code> decorator with subsequent <code>MyRecord.define()</code> needs to be used.</p>
<h3 id="attrdef-date"><code>attrDef</code> : Date</h3>
<p>Date attribute initialized as <code>new Date()</code>, and represented in JSON as UTC ISO string.</p>
<p>There are other popular Date serialization options available in <code>type-r/ext-types</code> package.</p>
<ul>
<li><code>MicrosoftDate</code> - Date serialized as Microsoft's <code>"/Date(msecs)/"</code> string.</li>
<li><code>Timestamp</code> - Date serializaed as UNIX integer timestamp (<code>date.getTime()</code>).</li>
</ul>
<pre><code class="highlight typescript"><span class="hljs-meta">@define</span> <span class="hljs-keyword">class</span> Person <span class="hljs-keyword">extends</span> Record {
<span class="hljs-meta">@auto</span> justDate : <span class="hljs-built_in">Date</span>
<span class="hljs-comment">// MicrosoftDate is an attribute metatype, not a real type, so you must pass it explictly.</span>
<span class="hljs-meta">@type</span>( Timestamp ).as createdAt : <span class="hljs-built_in">Date</span>
...
}
</code></pre>
<h3 id="static-collection"><code>static</code> Collection</h3>
<p>The default record's collection class automatically defined for every Record subclass. Can be referenced as <code>Record.Collection</code>.</p>
<p>May be explicitly assigned in record's definition with custom collection class.</p>
<pre><code class="highlight javascript"><span class="hljs-comment">// Declare the collection class.</span>
@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Comments</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Record</span>.<span class="hljs-title">Collection</span> </span>{}
@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Comment</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Record</span></span>{
<span class="hljs-keyword">static</span> Collection = Comments; <span class="hljs-comment">// Make it the default Comment collection.</span>
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">text</span> : <span class="hljs-built_in">String</span>,
<span class="hljs-attr">replies</span> : Comments
}
}
</code></pre>
<pre><code class="highlight typescript"><span class="hljs-comment">// Declare the collection class.</span>
<span class="hljs-meta">@define</span> <span class="hljs-keyword">class</span> Comments <span class="hljs-keyword">extends</span> Collection<Comment> {}
<span class="hljs-meta">@define</span> <span class="hljs-keyword">class</span> Comment <span class="hljs-keyword">extends</span> Record{
<span class="hljs-keyword">static</span> Collection = Comments; <span class="hljs-comment">// Make it the default Comment collection.</span>
<span class="hljs-meta">@auto</span> text : <span class="hljs-built_in">String</span>
<span class="hljs-meta">@auto</span> replies : Comments
}
</code></pre>
<h3 id="attrdef-type-type-"><code>attrDef</code> type(Type)</h3>
<p>Attribute definition can have different metadata attached which affects various aspects of attribute's behavior. Metadata is attached with
a chain of calls after the <code>type( Ctor )</code> call. Attribute's default value is the most common example of such a metadata and is the single option which can be applied to the constructor function directly.</p>
<pre><code class="highlight javascript"><span class="hljs-keyword">import</span> { define, type, Record }
@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">Record</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">a</span> : type( <span class="hljs-built_in">String</span> ).value( <span class="hljs-string">"a"</span> )
}
}
</code></pre>
<pre><code class="highlight typescript"><span class="hljs-keyword">import</span> { define, <span class="hljs-keyword">type</span>, Record }
<span class="hljs-meta">@define</span> <span class="hljs-keyword">class</span> Dummy <span class="hljs-keyword">extends</span> Record {
<span class="hljs-meta">@type</span>( <span class="hljs-built_in">String</span> ).value( <span class="hljs-string">"a"</span> ).as a : <span class="hljs-built_in">string</span>
}
</code></pre>
<h2 id="definitions-in-typescript">Definitions in TypeScript</h2>
<p>Type-R supports several options to define record attributes.</p>
<h3 id="decorator-auto"><code>decorator</code> @auto</h3>
<p>Turns TypeScript class property definition to the record's attribute, automatically extracting attribute type from the TypeScript type annotation. Requires <code>reflect-metadata</code> npm package and <code>emitDecoratorMetadata</code> option set to true in the <code>tsconfig.json</code>.</p>
<p><code>@auto</code> may take a single parameter as an attribute default value. No other attribute metadata can be attached.</p>
<pre><code class="highlight typescript"><span class="hljs-keyword">import</span> { define, auto, Record } <span class="hljs-keyword">from</span> <span class="hljs-string">'type-r'</span>
<span class="hljs-meta">@define</span> <span class="hljs-keyword">class</span> User <span class="hljs-keyword">extends</span> Record {
<span class="hljs-meta">@auto</span> name : <span class="hljs-built_in">string</span>
<span class="hljs-meta">@auto</span>( <span class="hljs-string">"john@verizon.com"</span> ) email : <span class="hljs-built_in">string</span>
<span class="hljs-meta">@auto</span>( <span class="hljs-literal">null</span> ) updatedAt : <span class="hljs-built_in">Date</span>
}
</code></pre>
<h3 id="decorator-attrdef-as"><code>decorator</code> @<code>attrDef</code>.as</h3>
<p>Attribute definition creates the TypeScript property decorator when being appended with <code>.as</code> suffix. It's an alternative syntax to <code>@auto</code>.</p>
<pre><code class="highlight typescript"><span class="hljs-keyword">import</span> { define, <span class="hljs-keyword">type</span>, Record } <span class="hljs-keyword">from</span> <span class="hljs-string">'type-r'</span>
<span class="hljs-meta">@define</span> <span class="hljs-keyword">class</span> User <span class="hljs-keyword">extends</span> Record {
<span class="hljs-meta">@value</span>( <span class="hljs-string">"5"</span> ).as name : <span class="hljs-built_in">string</span>
<span class="hljs-meta">@type</span>( <span class="hljs-built_in">String</span> ).toJSON( <span class="hljs-literal">false</span> ).as email : <span class="hljs-built_in">string</span>
}
</code></pre>
<h2 id="create-and-dispose">Create and dispose</h2>
<p>Record behaves as regular ES6 class with attributes accessible as properties.</p>
<h3 id="new-record-">new Record()</h3>
<p>Create an instance of the record with default attribute values taken from the attributes definition.</p>
<p>When no default value is explicitly provided for an attribute, it's initialized as <code>new Type()</code> (just <code>Type()</code> for primitives). When the default value is provided and it's not compatible with the attribute type, it's converted with <code>new Type( defaultValue )</code> call.</p>
<h3 id="new-record-attrname-value-options-">new Record({ attrName : value, ... }, options?)</h3>
<p>When creating an instance of a record, you can pass in the initial attribute values to override the defaults.</p>
<p>If <code>{parse: true}</code> option is used, <code>attrs</code> is assumed to be the JSON.</p>
<p>If the value of the particular attribute is not compatible with its type, it's converted to the declared type invoking the constructor <code>new Type( value )</code> (just <code>Type( value )</code> for primitives).</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">Record</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>
<pre><code class="highlight typescript"><span class="hljs-meta">@define</span> <span class="hljs-keyword">class</span> Book <span class="hljs-keyword">extends</span> Record {
<span class="hljs-meta">@auto</span> title : <span class="hljs-built_in">string</span>
<span class="hljs-meta">@auto</span> author : <span class="hljs-built_in">string</span>
}
<span class="hljs-keyword">const</span> book = <span class="hljs-keyword">new</span> Book({
title: <span class="hljs-string">"One Thousand and One Nights"</span>,
author: <span class="hljs-string">"Scheherazade"</span>
});
</code></pre>
<h3 id="record-clone-">record.clone()</h3>
<p>Create the deep copy of the aggregation tree, recursively cloning all aggregated records and collections. References to shared members will be copied, but not shared members themselves.</p>
<h3 id="callback-record-initialize-attrs-options-"><code>callback</code> record.initialize(attrs?, options?)</h3>
<p>Called at the end of the <code>Record</code> constructor when all attributes are assigned and the record's inner state is properly initialized. Takes the same arguments as
a constructor.</p>
<h3 id="record-dispose-">record.dispose()</h3>
<p>Recursively dispose the record 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="record-cid">record.cid</h3>
<p>Read-only client-side record's identifier. Generated upon creation of the record and is unique for every record's instance. Cloned records will have different <code>cid</code>.</p>
<h3 id="record-id">record.id</h3>
<p>Predefined record'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 record with the same <code>id</code> in the particular collection.</p>
<h3 id="record-isnew-">record.isNew()</h3>
<p>Has this record been saved to the server yet? If the record does not yet have an <code>id</code>, it is considered to be new.</p>
<h3 id="record-attrname">record.attrName</h3>
<p>Record's attributes may be directly accessed as <code>record.name</code>.</p>
<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">Account</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-built_in">String</span>,
<span class="hljs-attr">balance</span> : <span class="hljs-built_in">Number</span>
}
}
<span class="hljs-keyword">const</span> myAccount = <span class="hljs-keyword">new</span> Account({ <span class="hljs-attr">name</span> : <span class="hljs-string">'mine'</span> });
myAccount.balance += <span class="hljs-number">1000000</span>; <span class="hljs-comment">// That works. Good, eh?</span>
</code></pre>
<h3 id="record-attrname-value">record.attrName = value</h3>
<p>Assign the record's attribute. If the value is not compatible with attribute's type from the declaration, it is converted:</p>
<ul>
<li>with <code>Type( value )</code> call, for primitive types;</li>
<li>with <code>record.attrName.set( value )</code>, for existing record or collection (updated in place);</li>
<li>with <code>new Type( value )</code> in all other cases.</li>
</ul>
<p>Record triggers events on changes:</p>
<ul>
<li><code>change:attrName</code> <em>( record, value )</em>.</li>
<li><code>change</code> <em>( record )</em>.</li>
</ul>
<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">Record</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>
<h3 id="record-set-attrname-value-options-options-">record.set({ attrName : value, ... }, options? : <code>options</code>)</h3>
<p>Bulk assign record's attributes, possibly taking options.</p>
<p>If the value is not compatible with attribute's type from the declaration, it is converted:</p>
<ul>
<li>with <code>Type( value )</code> call, for primitive types.</li>
<li>with <code>record.attrName.set( value )</code>, for existing record or collection (updated in place).</li>
<li>with <code>new Type( value )</code> in all other cases.</li>
</ul>
<p>Record triggers events after all changes are applied:</p>
<ol>
<li><code>change:attrName</code> <em>( record, val, options )</em> for any changed attribute.</li>
<li><code>change</code> <em>(record, options)</em>, if there were changed attributes.</li>
</ol>
<h3 id="recordclass-from-attrs-options-">RecordClass.from(attrs, options?)</h3>
<p>Create <code>RecordClass</code> from attributes. Similar to direct record creation, but supports additional option for strict data validation.
If <code>{ strict : true }</code> option is passed the validation will be performed and an exception will be thrown in case of an error.</p>
<p>Please note, that 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 they are 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 record 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>
<pre><code class="highlight typescript"><span class="hljs-comment">// Fetch record with a given id.</span>
<span class="hljs-keyword">const</span> book = <span class="hljs-keyword">await</span> Book.from({ id : <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, { parse : <span class="hljs-literal">true</span>, strict : <span class="hljs-literal">true</span> });
</code></pre>
<h3 id="record-assignfrom-otherrecord-">record.assignFrom(otherRecord)</h3>
<p>Makes an existing <code>record</code> to be the full clone of <code>otherRecord</code>, recursively assigning all attributes.
In contracts to <code>record.clone()</code>, the record is updated in place.</p>
<pre><code class="highlight javascript"><span class="hljs-comment">// Another way of doing the bestSeller.clone()</span>
<span class="hljs-keyword">const</span> book = <span class="hljs-keyword">new</span> Book();
book.assignFrom(bestSeller);
</code></pre>
<h3 id="record-transaction-fun-">record.transaction(fun)</h3>
<p>Execute the all changes made to the record in <code>fun</code> as single transaction triggering the single <code>change</code> event.</p>
<p>All record updates occurs in the scope of transactions. Transaction is the sequence of changes which results in a single <code>change</code> event.
Transaction can be opened either manually or implicitly with calling <code>set()</code> or assigning an attribute.
Any additional changes made to the record in <code>change:attr</code> event handler will be executed in the scope of the original transaction, and won't trigger additional <code>change</code> events.</p>
<pre><code class="highlight javascript">some.record.transaction( <span class="hljs-function"><span class="hljs-params">record</span> =></span> {
record.a = <span class="hljs-number">1</span>; <span class="hljs-comment">// `change:a` event is triggered.</span>
record.b = <span class="hljs-number">2</span>; <span class="hljs-comment">// `change:b` event is triggered.</span>
}); <span class="hljs-comment">// `change` event is triggered.</span>
</code></pre>
<p>Manual transactions with attribute assignments are superior to <code>record.set()</code> in terms of both performance and flexibility.</p>
<h3 id="attrdef-type-type-get-hook-"><code>attrDef</code> : type(Type).get(<code>hook</code>)</h3>
<p>Attach get hook to the record's attribute. <code>hook</code> is the function of signature <code>( value, attr ) => value</code> which is used to transform the attribute's value <em>before it will be read</em>. Hook is executed in the context of the record.</p>
<h3 id="attrdef-type-type-set-hook-"><code>attrDef</code> : type(Type).set(<code>hook</code>)</h3>
<p>Attach the set hook to the record's attribute. <code>hook</code> is the function of signature <code>( value, attr ) => value</code> which is used to transform the attribute's value <em>before it will be assigned</em>. Hook is executed in the context of the record.</p>
<p>If set hook will return <code>undefined</code>, it will cancel attribute update.</p>
<h2 id="nested-records-and-collections">Nested records and collections</h2>
<p>Record's attributes can hold other Records and Collections, forming indefinitely nested data structures of arbitrary complexity.
To create nested record or collection you should just mention its constructor function in attribute's definition.</p>
<pre><code class="highlight javascript"><span class="hljs-keyword">import</span> { Record } <span class="hljs-keyword">from</span> <span class="hljs-string">'type-r'</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-built_in">String</span>,
<span class="hljs-attr">email</span> : <span class="hljs-built_in">String</span>,
<span class="hljs-attr">isActive</span> : <span class="hljs-literal">true</span>
}
}
@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UsersListState</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">users</span> : User.Collection
}
}
</code></pre>
<p>All nested records and collections are <em>aggregated</em> by default and behave as integral parts of the containing record. Aggregated attributes are <em>exclusively owned</em> by the record, and taken with it together form an <em>ownership tree</em>. Many operations are performed recursively on aggregated elements:</p>
<ul>
<li>They are created when the owner record is created.</li>
<li>They are cloned when the record is cloned.</li>
<li>They are disposed when the record is disposed.</li>
<li>They are validated as part of the record.</li>
<li>They are serialized as nested JSON.</li>
</ul>
<p>The nature of aggregation relationship in OO is explained in this <a href="https://medium.com/@gaperton/nestedtypes-2-0-meet-an-aggregation-and-the-rest-of-oo-animals-a9fca7c36ecf">article</a>.</p>
<h3 id="attrdef-recordorcollection"><code>attrDef</code> : RecordOrCollection</h3>
<p>Aggregated record or collection. Represented as nested object or array in record's JSON. Aggregated members are owned by the record and treated as its <em>integral part</em> (recursively created, cloned, serialized, validated, and disposed).
One object can have single owner. The record with its aggregated attributes forms an <em>aggregation tree</em>.</p>
<p>All changes in aggregated record or collections are detected and cause change events on the containing record.</p>
<h3 id="record-getowner-">record.getOwner()</h3>
<p>Return the record which is an owner of the current record, or <code>null</code> there are no one.</p>
<p>Due to the nature of <em>aggregation</em>, an object may have one and only one owner.</p>
<h3 id="record-collection">record.collection</h3>
<p>Return the collection which aggregates the record, or <code>null</code> if there are no one.</p>
<h3 id="attrdef-shared-recordorcollection-"><code>attrDef</code> : shared(RecordOrCollection)</h3>
<p>Non-serializable reference to the record or collection possibly from the different aggregation tree. Initialized with <code>null</code>. Is not recursively cloned, serialized, validated, or disposed.</p>
<p>All changes in shared records or collections are detected and cause change events of the containing record.</p>
<aside class="notice">The type of <code>attrDef</code>{ name : defaultValue } is inferred as `shared( Type )` if it extends Record or Collection</aside>
<pre><code class="highlight javascript">@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UsersListState</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">users</span> : User.Collection,
<span class="hljs-attr">selected</span> : shared( User ) <span class="hljs-comment">// Can be assigned with the user from this.users</span>
}
}
</code></pre>
<pre><code class="highlight typescript"><span class="hljs-meta">@define</span> <span class="hljs-keyword">class</span> UsersListState <span class="hljs-keyword">extends</span> Record {
<span class="hljs-meta">@type</span>( User.Collection ).as users : Collection<User>,
<span class="hljs-meta">@shared</span>( User ).as selected : User <span class="hljs-comment">// Can be assigned with the user from this.users</span>
}
</code></pre>
<h3 id="attrdef-collection-refs"><code>attrDef</code> : Collection.Refs</h3>
<p>Non-aggregating collection. Collection of references to shared records which itself is <em>aggregated</em> by the record, but <em>does not aggregate</em> its elements. In contrast to the <code>shared( Collection )</code>, <code>Collection.Refs</code> is an actual constructor and creates an instance of collection which <em>is the part the parent record</em>.</p>
<p>The collection itself is recursively created and cloned. However, its records are not aggregated by the collection thus they are not recursively cloned, validated, serialized, or disposed.</p>
<p>All changes in the collection and its elements are detected and cause change events of the containing record.</p>
<aside class="notice"><code>Collection.Refs</code> is the constructor and can be used to create non-aggregating collection with `new` operator.</aside>
<pre><code class="highlight javascript"> @define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyRecord</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">notCloned</span> : shared( SomeCollection ), <span class="hljs-comment">// Reference to the _shared collection_ object.</span>
cloned : SomeCollection.Refs <span class="hljs-comment">// _Aggregated_ collection of references to the _shared records_.</span>
}
}
</code></pre>
<pre><code class="highlight typescript"> <span class="hljs-meta">@define</span> <span class="hljs-keyword">class</span> MyRecord <span class="hljs-keyword">extends</span> Record {
<span class="hljs-comment">// Reference to the _shared collection_ object.</span>
<span class="hljs-meta">@shared</span>( SomeCollection ).as notCloned : Collection<Some>
<span class="hljs-comment">// _Aggregated_ collection of references to the _shared records_.</span>
<span class="hljs-meta">@type</span>( SomeCollection.Refs ).as cloned : SomeCollection
}
</code></pre>
<h3 id="decorator-predefine"><code>decorator</code> @predefine</h3>
<p>Make forward declaration for the record to define its attributes later with <code>RecordClass.define()</code>. Used instead of <code>@define</code> for recursive record definitions.</p>
<p>Creates the default <code>RecordClass.Collection</code> type which can be referenced in attribute definitions.</p>
<h3 id="static-define-attributes-name-attrdef-"><code>static</code> define({ attributes : { name : <code>attrDef</code>, ... }})</h3>
<p>May be called to define attributes in conjunction with <code>@predefine</code> decorator to make recursive record definitions.</p>
<pre><code class="highlight javascript">@predefine <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Comment</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Record</span></span>{}
Comment.define({
<span class="hljs-attr">attributes</span> : {
<span class="hljs-attr">text</span> : <span class="hljs-built_in">String</span>,
<span class="hljs-attr">replies</span> : Comment.Collection
}
});
</code></pre>
<h1 id="collection">Collection</h1>
<p>Collections are ordered sets of records. The collection is an array-like object exposing ES6 Array and BackboneJS Collection interface. It encapsulates JS Array of records (<code>collection.models</code>) and a hashmap for a fast O(1) access by the record <code>id</code> and <code>cid</code> (<code>collection.get( id )</code>).</p>
<p>Collactions are deeply observable. You can bind "changes" events to be notified when the collection has been modified, listen for the record "add", "remove", and "change" events.</p>
<p>Every <code>Record</code> class has an implicitly defined <code>Collection</code> accessible as a static member of a record's constructor. In a most cases, you don't need to define the custom collection class.</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">Record</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">title</span> : <span class="hljs-built_in">String</span>
author : Author
}
}
<span class="hljs-comment">// Implicitly defined collection.</span>
<span class="hljs-keyword">const</span> books = <span class="hljs-keyword">new</span> Book.Collection();
</code></pre>
<pre><code class="highlight typescript"><span class="hljs-meta">@define</span> <span class="hljs-keyword">class</span> Book <span class="hljs-keyword">extends</span> Record {
<span class="hljs-meta">@auto</span> title : <span class="hljs-built_in">string</span>
<span class="hljs-meta">@auto</span> author : Author
<span class="hljs-comment">// Tell TypeScript the proper type.</span>
<span class="hljs-keyword">static</span> Collection : CollectionConstructor<Book>
}
<span class="hljs-keyword">const</span> books = <span class="hljs-keyword">new</span> Book.Collection();
</code></pre>
<p>You can define custom collection classes extending <code>Record.Collection</code> or any other collection class. It can either replace the default Collection type, or </p>
<pre><code class="highlight javascript"><span class="hljs-comment">// Define custom collection class.</span>
@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Library</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Record</span>.<span class="hljs-title">Collection</span> </span>{
doSomething(){ ... }
}
@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">Record</span> </span>{
<span class="hljs-comment">// Override the default collection.</span>
<span class="hljs-keyword">static</span> Collection = Library;
}
<span class="hljs-comment">// Define another custom collection class.</span>
@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">OtherLibrary</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Record</span>.<span class="hljs-title">Collection</span> </span>{
<span class="hljs-comment">// Specify the record so the collection will be able to restore itself from JSON.</span>
<span class="hljs-keyword">static</span> model = Book;
}
</code></pre>
<pre><code class="highlight typescript"><span class="hljs-comment">// Define custom collection class.</span>
<span class="hljs-meta">@define</span> <span class="hljs-keyword">class</span> Library <span class="hljs-keyword">extends</span> Collection<Book> {
doSomething(){ ... }
}
<span class="hljs-meta">@define</span> <span class="hljs-keyword">class</span> Book <span class="hljs-keyword">extends</span> Record {
<span class="hljs-comment">// Override the default collection.</span>
<span class="hljs-keyword">static</span> Collection = Library;
}
<span class="hljs-comment">// Define another custom collection class.</span>
<span class="hljs-meta">@define</span> <span class="hljs-keyword">class</span> OtherLibrary <span class="hljs-keyword">extends</span> Collection<Book> {
<span class="hljs-comment">// Specify the record so the collection will be able to restore itself from JSON.</span>
<span class="hljs-keyword">static</span> model = Book;
}
<span class="hljs-comment">// An alternative way of overriding the default collection class in TypeScript.</span>
<span class="hljs-keyword">namespace</span> Book {
<span class="hljs-meta">@define</span> <span class="hljs-keyword">class</span> Collection <span class="hljs-keyword">extends</span> Collection<Book> {
<span class="hljs-keyword">static</span> model = Book;
}
}
</code></pre>
<aside class="notice">
The collection must know the type of its records to restore its elements from JSON properly. When the `model` is not specified, the collection can hold any Record subclass but it cannot deserialize itself.
</aside>
<h2 id="collection-types">Collection types</h2>
<h3 id="constructor-collectionclass-records-options-"><code>constructor</code> CollectionClass( records?, options? )</h3>
<p>The most common collection type is an <strong>aggregating serializable collection</strong>. By default, collection aggregates its elements which are treated as an integral part of the collection (serialized, cloned, disposed, and validated recursively). An aggregation means the <em>single owner</em>, as the single object cannot be an integral part of two distinct things. The collection will take ownership on its records and will put an error in the console if it can't.</p>
<p>When creating a Collection, you may choose to pass in the initial array of records.</p>
<pre><code class="highlight javascript">@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Role</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-built_in">String</span>
}
}
<span class="hljs-keyword">const</span> roles = <span class="hljs-keyword">new</span> Role.Collection( json, { <span class="hljs-attr">parse</span> : <span class="hljs-literal">true</span> } );
</code></pre>
<pre><code class="highlight typescript"><span class="hljs-meta">@define</span> <span class="hljs-keyword">class</span> Role <span class="hljs-keyword">extends</span> Record {
<span class="hljs-comment">// In typescript, you have to specify record's Collection type expicitly.</span>
<span class="hljs-keyword">static</span> Collection : CollectionConstructor<Role>
<span class="hljs-meta">@auto</span> name : <span class="hljs-built_in">string</span>
}
<span class="hljs-meta">@define</span> <span class="hljs-keyword">class</span> User <span class="hljs-keyword">extends</span> Record {
<span class="hljs-meta">@auto</span> name : <span class="hljs-built_in">string</span>
<span class="hljs-comment">// Type-R cannot infer a Collection metatype from the TypeScript type automatically.</span>
<span class="hljs-comment">// Full attribute type annotation is required.</span>
<span class="hljs-meta">@type</span>( Role.Collection ).as roles : Collection<User>
}
</code></pre>
<h3 id="constructor-collectionclass-refs-records-options-"><code>constructor</code> CollectionClass.Refs( records?, options? )</h3>
<p>Collection of record references is a <strong>non-aggregating non-serializable collection</strong>. <code>Collection.Refs</code> doesn't aggregate its elements, which means that containing records are not considered as an integral part of the enclosing collection and not being validated, cloned, disposed, and serialized recursively.</p>
<p>It is useful for a local non-persistent application state.</p>
<h3 id="attrdef-subsetof-masterref-collectionclass-"><code>attrDef</code> subsetOf(masterRef, CollectionClass?)</h3>
<p>The subset of other collections are <strong>non-aggregating serializable collection</strong>. Subset-of collection is serialized as an array of record ids and used to model many-to-many relationships. The collection object itself is recursively created and cloned, however, its records are not aggregated by the collection thus they are not recursively cloned, validated, or disposed. <code>CollectionClass</code> argument may be omitted unless you need the record's attribute to be an instance of the particular collection class.</p>
<aside class="notice">
<b>subsetOf</b> collections are not deeply observable.
</aside>
<aside class="notice">
Since its an attribute <i>metatype</i> (combination of type and attribute metadata), it's not a real constructor and cannot be used with <b>new</b>. Use <b>collection.createSubset()</b> method to create subset-of collection instances.
</aside>
<p>Must have a reference to the master collection which is used to resolve record ids to records. <code>masterRef</code> may be:</p>
<ul>
<li>direct reference to a singleton collection.</li>
<li>function, returning the reference to the collection.</li>
<li>symbolic dot-separated path to the master collection resolved relative to the record's <code>this</code>. You may use <code>owner</code> and <code>store</code> macro in path:<ul>
<li><code>owner</code> is the reference to the record's owner. <code>owner.some.path</code> works as <code>() => this.getOwner().some.path</code>.</li>
<li><code>store</code> is the reference to the closes store. <code>store.some.path</code> works as <code>() => this.getStore().some.path</code>.</li>
</ul>
</li>
</ul>
<pre><code class="highlight javascript">@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Role</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-built_in">String</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-built_in">String</span>,
<span class="hljs-attr">roles</span> : subsetOf( <span class="hljs-string">'owner.roles'</span>, Role.Collection )
}
}
@define <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">UsersDirectory</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Store</span> </span>{
<span class="hljs-keyword">static</span> attributes = {
<span class="hljs-attr">roles</span> : Role.Collection,
<span class="hljs-attr">users</span> : User.Collection <span class="hljs-comment">// `~roles` references will be resolved against this.roles</span>
}
}
</code></pre>
<pre><code class="highlight typescript"><span class="hljs-meta">@define</span> <span class="hljs-keyword">class</span> Role <span class="hljs-keyword">extends</span> Record {
<span class="hljs-keyword">static</span> Collection : CollectionConstructor<Role>
<span class="hljs-meta">@auto</span> name : <span class="hljs-built_in">string</span>
...
}
<span class="hljs-meta">@define</span> <span class="hljs-keyword">class</span> User <span class="hljs-keyword">extends</span> Record {
<span class="hljs-keyword">static</span> Collection : CollectionConstructor<User>
<span class="hljs-meta">@auto</span> name : <span class="hljs-built_in">string</span>
<span class="hljs-meta">@subsetOf</span>(<span class="hljs-string">'store.roles'</span>).as roles : Collection<Role>
}
<span class="hljs-meta">@define</span> <span class="hljs-keyword">class</span> UsersDirectory <span class="hljs-keyword">extends</span> Store {
<span class="hljs-meta">@type</span>(Role.Collection).as roles : Collection<Role>,
<span class="hljs-meta">@type</span>(User.Collection).as users : Collection<User> <span class="hljs-comment">// <- `store.roles` references will be resolved against this.roles</span>
}
</code></pre>
<h2 id="array-api">Array API</h2>
<p>A collection class is an array-like object implementing ES6 Array methods and properties.</p>
<h3 id="collection-length">collection.length</h3>
<p>Like an array, a Collection maintains a length property, counting the number of records it contains.</p>
<h3 id="collection-slice-begin-end-">collection.slice( begin, end )</h3>
<p>Return a shallow copy of the <code>collection.models</code>, using the same options as native Array#slice.</p>
<h3 id="collection-indexof-recordorid-any-number">collection.indexOf( recordOrId : any ) : number</h3>
<p>Return an index of the record in the collection, and -1 if there is no such a record in the collection.</p>
<p>Can take the record itself as an argument, <code>id</code>, or <code>cid</code> of the record.</p>
<h3 id="collection-foreach-iteratee-val-record-index-void-context-">collection.forEach( iteratee : ( val : Record, index ) => void, context? )</h3>
<p>Iterate through the elements of the collection.</p>
<aside class="notice">Use <code>collection.updateEach( iteratee, index )</code> method to update records in a loop.</aside>
<h3 id="collection-map-iteratee-val-record-index-t-context-">collection.map( iteratee : ( val : Record, index ) => T, context? )</h3>
<p>Map elements of the collection. Similar to <code>Array.map</code>.</p>
<h3 id="collection-filter-iteratee-predicate-context-">collection.filter( iteratee : Predicate, context? )</h3>
<p>Return the filtered array of records matching the predicate.</p>
<p>The predicate is either the iteratee function returning boolean, or an object with attribute values used to match with record's attributes.</p>
<h3 id="collection-every-iteratee-predicate-context-boolean">collection.every( iteratee : Predicate, context? ) : boolean</h3>
<p>Return <code>true</code> if all records match the predicate.</p>
<h3 id="collection-some-iteratee-predicate-context-boolean">collection.some( iteratee : Predicate, context? ) : boolean</h3>
<p>Return <code>true</code> if at least one record matches the predicated.</p>
<h3 id="collection-push-record-options-">collection.push( record, options? )</h3>
<p>Add a record at the end of a collection. Takes the same options as <code>add()</code>.</p>
<h3 id="collection-pop-options-">collection.pop( options? )</h3>
<p>Remove and return the last record from a collection. Takes the same options as <code>remove()</code>.</p>
<h3 id="collection-unshift-record-options-">collection.unshift( record, options? )</h3>
<p>Add a record at the beginning of a collection. Takes the same options as <code>add()</code>.</p>
<h3 id="collection-shift-options-">collection.shift( options? )</h3>
<p>Remove and return the first record from a collection. Takes the same options as <code>remove()</code>.</p>
<h2 id="backbone-api">Backbone API</h2>
<p>Common options used by Backbone API methods:</p>
<ul>
<li><code>{ sort : false }</code> - do not sort the collection.</li>