objectmodel 4.2.3 → 4.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.html CHANGED
@@ -1,1606 +1,1604 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
-
4
- <head>
5
- <meta charset="utf-8">
6
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
7
- <meta name="viewport" content="width=device-width, initial-scale=1">
8
- <meta name="description" content="ObjectModel : Strong Dynamically Typed Object Modeling for JavaScript">
9
- <meta name="author" content="Sylvain Pollet-Villard">
10
-
11
- <title>ObjectModel</title>
12
-
13
- <!-- Global site tag (gtag.js) - Google Analytics -->
14
- <script async src="https://www.googletagmanager.com/gtag/js?id=UA-74235687-2"></script>
15
- <script>
16
- window.dataLayer = window.dataLayer || [];
17
-
18
- function gtag() {
19
- dataLayer.push(arguments);
20
- }
21
- gtag('js', new Date());
22
-
23
- gtag('config', 'UA-74235687-2');
24
- </script>
25
-
26
- <link rel="icon" type="image/png" href="docs/favicon-32x32.png" sizes="32x32" />
27
- <link rel="icon" type="image/png" href="docs/favicon-16x16.png" sizes="16x16" />
28
- <link rel="shortcut icon" href="docs/favicon.ico" type="image/x-icon">
29
-
30
- <link rel="stylesheet" href="docs/style/main.compiled.css" />
31
-
32
- <script defer src="docs/js/main.compiled.js"></script>
33
- <script async type="module">
34
- import * as globals from "./dist/object-model.js"
35
- Object.assign(window, globals)
36
- </script>
37
- </head>
38
-
39
- <body>
40
- <button id="menu-button" class="lines-button arrow arrow-left" aria-label="Toggle Navigation">
41
- <span class="lines"></span>
42
- </button>
43
-
44
- <nav id="menu">
45
- <a class="title download-link" href="#download">
46
- <h2>Download</h2>
47
- </a>
48
- <hr>
49
- <a class="title github-link" target="_blank" rel="noopener"
50
- href="https://github.com/sylvainpolletvillard/ObjectModel">
51
- <h2>View on Github</h2>
52
- </a>
53
- <hr>
54
- <div>
55
- <a href="#introduction" class="title">Introduction</a>
56
- <ul>
57
- <li><a href="#introduction">What is it</a></li>
58
- <li><a href="#video-demo">Video demo</a></li>
59
- <li><a href="#features">Features</a></li>
60
- </ul>
61
- </div>
62
- <hr>
63
- <div>
64
- <a href="#doc-basic-model" class="title">Documentation</a>
65
- <ul>
66
- <li><a href="#doc-basic-model">Basic models</a></li>
67
- <li><a href="#doc-object-model">Object models</a></li>
68
- <li><a href="#doc-model">Model constructor</a></li>
69
- <li><a href="#doc-es6-classes">Using ES6 class</a></li>
70
- <li><a href="#doc-optional-properties">Optional properties</a></li>
71
- <li><a href="#doc-multiple-types">Multiple types</a></li>
72
- <li><a href="#doc-value-checking">Value checking and enumerations</a></li>
73
- <li><a href="#doc-null-safe">Null-safe object traversal</a></li>
74
- <li><a href="#doc-default-values">Default values</a></li>
75
- <li><a href="#doc-composition">Composition</a></li>
76
- <li><a href="#doc-extensions">Inheritance by extensions</a></li>
77
- <li><a href="#doc-multiple-inheritance">Multiple inheritance</a></li>
78
- <li><a href="#doc-assertions">Assertions for custom tests</a></li>
79
- <li><a href="#doc-private-and-constants">Private and constant properties</a></li>
80
- <li><a href="#doc-array-model">Array models</a></li>
81
- <li><a href="#doc-function-model">Function models</a></li>
82
- <li><a href="#doc-map-models">Map models</a></li>
83
- <li><a href="#doc-set-models">Set models</a></li>
84
- <li><a href="#doc-any-model">Any model</a></li>
85
- <li><a href="#doc-custom-collectors">Custom error collectors</a></li>
86
- <li><a href="#doc-custom-devtool-formatters">Custom devtool formatters</a></li>
87
- <li><a href="#api">Full API</a></li>
88
- <li><a href="#common-models">Commonly used models</a></li>
89
- <li><a href="#common-questions">Common questions</a></li>
90
- </ul>
91
- </div>
92
- <hr>
93
- </nav>
94
-
95
- <div id="page">
96
-
97
- <header class="header">
98
- <img id="logo" src="docs/res/logo.png" alt="ObjectModel" width="800" height="150">
99
- <hr>
100
- <p class="description">Strong Dynamically Typed Object Modeling for JavaScript</p>
101
- </header>
102
-
103
- <section id="introduction" class="grid">
104
- <h1 hidden>Introduction</h1>
105
-
106
- <div class="description">
107
- <h2 id="what-is-it">What is this library ?</h2>
108
-
109
- <p>ObjectModel intends to bring <strong>strong dynamic type checking</strong> to your web applications.
110
- Contrary to static type-checking solutions like <a href="https://www.typescriptlang.org"
111
- target="_blank" rel="noopener">TypeScript</a> or <a href="https://flowtype.org" target="_blank"
112
- rel="noopener">Flow</a>, ObjectModel can also validate data at runtime: JSON from the server,
113
- form inputs, content from local storages, external libraries...</p>
114
-
115
- <p>By leveraging <strong>ES6 Proxies</strong>, this library ensures that your variables always match the
116
- model definition and validation constraints you added to them. Thanks to the generated exceptions,
117
- it will help you spot potential bugs and save you time spent on debugging. ObjectModel is also very
118
- easy to master: no new language to learn, no new tools, no compilation step, just a minimalist and
119
- intuitive API in a plain old JS micro-library.</p>
120
-
121
- <p>Validating at runtime also brings many other benefits: you can define your own types, use them in
122
- complex model definitions with custom assertions that can even change depending on your application
123
- state. Actually it goes much further than just type safety. Go on and see for yourself.</p>
124
- </div>
125
-
126
- <div id="video-demo">
127
- <iframe data-src="https://www.youtube.com/embed/zmojfyNH_EE?rel=0&amp;showinfo=0" src="" width="640"
128
- height="360" frameborder="0" allowfullscreen>
129
- </iframe>
130
- </div>
131
- </section>
132
-
133
- <hr>
134
-
135
- <section id="features-and-download" class="grid">
136
- <div id="features">
137
- <h2>What's inside the box ?</h2>
138
- <p>Many features, hopefully neither too much nor too few:</p>
139
- <ul>
140
- <li>Typed structures: objects, arrays, maps, sets, functions...</li>
141
- <li>Union types</li>
142
- <li>Enumerations</li>
143
- <li>Custom assertions</li>
144
- <li>Optional properties</li>
145
- <li>Default values</li>
146
- <li>Null-safe object traversal</li>
147
- <li>Easy composition or inheritance</li>
148
- <li>Constants and private properties based on name conventions</li>
149
- <li>Explicit error messages</li>
150
- <li>Customizable error handlers</li>
151
- <li>all in <strong class="size-gzip">3.99 KB</strong> minified and gzipped,
152
- even less when using tree-shaking
153
- </li>
154
- </ul>
155
- </div>
156
-
157
- <div id="download">
158
- <h2>Download</h2>
159
- <h3>Current version: v<span class="version">4.2.2</span></h3>
160
- <ul>
161
- <li>
162
- From <a href="https://www.npmjs.com/package/objectmodel" target="_blank" rel="noopener">
163
- <abbr title="Node Package Manager">npm</abbr></a>:
164
- <code>npm install objectmodel</code>
165
- </li>
166
- <li>From <abbr title="Content Delivery Network">CDN</abbr>:
167
- <a href="http://cdn.pika.dev/objectmodel" rel="noopener">cdn.pika.dev/objectmodel</a>
168
- </li>
169
- <li>Minified <abbr title="ECMAScript module">ESM</abbr> bundle
170
- (<strong class="size-gzip">3.99 KB</strong> gzipped) :
171
- <a href="dist/object-model.min.js">object-model.min.js</a>
172
- </li>
173
- <li>Source files :
174
- <a class="link-zip" href="https://github.com/sylvainpolletvillard/ObjectModel/archive/v4.2.2.zip">object-model-4.2.2.zip</a>
175
- </li>
176
- </ul>
177
-
178
- <h3>Previous versions</h3>
179
-
180
- <p>If you need to support older browsers, you may have to use an older version of the library instead.
181
- Please take a look at the <a href="#common-questions"
182
- onclick="document.getElementById('browser-support').parentNode.open=true">
183
- Browsers/Node support</a>
184
- section for more information about browser/Node support for each version.
185
- </p>
186
- <p>Full changelog between versions is available on the
187
- <a href="https://github.com/sylvainpolletvillard/ObjectModel/releases" target="_blank"
188
- rel="noopener">Github Releases</a>
189
- page.</p>
190
-
191
- </div>
192
- <div>
193
- <h2>Usage</h2>
194
- <p>Since v4.0, ObjectModel is shipped in <abbr title="ECMAScript module">ESM</abbr> format, and written
195
- in modern JavaScript (ES2018).</strong></p>
196
-
197
- <div class="panel">
198
- <pre><code class="language-javascript">import { Model } from "objectmodel"</code></pre>
199
- </div>
200
-
201
- <p>Not all environments support <abbr title="ECMAScript modules">ESM</abbr> yet, so you should configure
202
- a transpiler/bundler such as
203
- Babel/Webpack for your project. If you just want a ready-to-use
204
- <abbr title="Universal Module Definition">UMD</abbr> version, you can use a transpiling service
205
- such as <a href="http://pika.dev" target="_blank" rel="noopener">pika.dev</a> or
206
- <a href="http://unpkg.com" target="_blank" rel="noopener">unpkg.com</a> like this:
207
- </p>
208
-
209
- <div class="panel">
210
- <pre><code class="language-html">&lt;script src="https://umd.cdn.pika.dev/objectmodel/v4"&gt;&lt;/script&gt;</code>
211
- <code class="language-js">const { Model } = objectmodel</code></pre>
212
- </div>
213
- </div>
214
- </section>
215
-
216
- <hr>
217
-
218
- <h1>Documentation</h1>
219
- <p class="tip">ObjectModel is already loaded on this webpage, so you can try the examples below in your browser
220
- JavaScript console.</p>
221
-
222
- <section id="doc-basic-model" class="grid doc-code-code">
223
- <div class="doc">
224
- <h2>Basic models</h2>
225
- <p>Basic models simply validate a variable against the model definition passed as argument, and return
226
- the validated value. <code>BasicModel</code> constructor takes a <i>model definition</i> as the only
227
- argument. They are generally used to declare all the basic generic types that you will use in your
228
- application. You can find a list of <a href="#common-models">common basic models here</a>.</p>
229
- </div>
230
-
231
- <div class="panel panel1">
232
- <span class="legend">Model</span>
233
- <pre><code class="language-javascript">import { BasicModel } from "objectmodel"
234
-
235
- const NumberModel = BasicModel(Number);
236
- // 'new' keyword is optional for models and model instances</code></pre>
237
- </div>
238
-
239
- <div class="panel panel2">
240
- <span class="legend">Instance</span>
241
- <pre><code class="language-javascript">let x = NumberModel("42");</code>
242
- <code class="language-none exception">TypeError: expecting Number, got String "42"</code></pre>
243
- </div>
244
- </section>
245
-
246
- <hr>
247
-
248
- <section id="doc-object-model" class="grid doc-code-code">
249
- <div class="doc">
250
- <h2>Object models</h2>
251
- <p>Object models validate nested object properties against a definition tree. They provide automatic
252
- validation at initial and future assignments of the properties of the instance objects.</p>
253
- </div>
254
-
255
- <div class="panel panel1">
256
- <span class="legend">Model</span>
257
- <pre><code class="language-javascript">import { ObjectModel } from "objectmodel"
258
-
259
- const Order = new ObjectModel({
260
- product: {
261
- name: String,
262
- quantity: Number,
263
- },
264
- orderDate: Date
265
- });</code></pre>
266
- </div>
267
-
268
- <div class="panel panel2">
269
- <span class="legend">Instance</span>
270
- <pre><code class="language-javascript">const myOrder = new Order({
271
- product: { name: "Apple Pie", quantity: 1 },
272
- orderDate: new Date()
273
- });
274
-
275
- myOrder.product.quantity = 2; // no exceptions thrown
276
- myOrder.product.quantity = false; //try to assign a Boolean</code>
277
- <code class="language-none exception">TypeError: expecting product.quantity to be Number, got Boolean false</code></pre>
278
- </div>
279
- </section>
280
-
281
- <hr>
282
-
283
- <section id="doc-model" class="grid doc-code">
284
- <div class="doc">
285
- <h2>Model constructor</h2>
286
- <p><code>Model</code> is the <strong>base class of all models</strong> and can be used as an alias for
287
- <code>BasicModel</code> and <code>ObjectModel</code> constructors.</p>
288
- </div>
289
-
290
- <div class="panel">
291
- <span class="legend">Example</span>
292
- <pre><code class="language-javascript">import { Model, BasicModel, ObjectModel } from "objectmodel"
293
-
294
- Model(String) // same as BasicModel(String)
295
- Model({ name: String }) // same as ObjectModel({ name: String })</code></pre>
296
- </div>
297
- </section>
298
-
299
- <hr>
300
-
301
- <section id="doc-es6-classes" class="grid doc-code-code">
302
- <div class="doc">
303
- <h2>Usage with ES6 classes</h2>
304
- <p>If you are using ES6 classes in your project, it is very easy to define a model for your classes:</p>
305
- </div>
306
-
307
- <div class="panel panel1">
308
- <span class="legend">Model</span>
309
- <pre><code class="language-javascript">class Character extends Model({ lastName: String, firstName: String }){
310
- get fullName(){ return `${this.firstName} ${this.lastName}`; }
311
- }</code></pre>
312
- </div>
313
-
314
- <div class="panel panel2">
315
- <span class="legend">Instance</span>
316
- <pre><code class="language-javascript">const rick = new Character({ lastName: "Sanchez", firstName: "Rick" });
317
- rick.lastName = 132;</code>
318
- <code class="language-none exception">TypeError: expecting lastName to be String, got Number 132</code>
319
- <code class="language-javascript">console.log(rick.fullName); // "Rick Sanchez"</code></pre>
320
- </div>
321
- </section>
322
-
323
- <hr>
324
-
325
- <section id="doc-optional-properties" class="grid doc-code-code">
326
- <div class="doc">
327
- <h2>Optional properties</h2>
328
- <p>By default, model properties are mandatory. That means all properties defined are required on
329
- instance declaration, otherwise an exception will be raised. But you can specify a property to be
330
- optional by using the bracket notation, borrowed from the JSDoc specification</p>
331
- </div>
332
-
333
- <div class="panel panel1">
334
- <span class="legend">Model</span>
335
- <pre><code class="language-javascript">const User = ObjectModel({
336
- email: String, // mandatory
337
- name: [String] // optional
338
- });</code></pre>
339
- </div>
340
-
341
- <div class="panel panel2">
342
- <span class="legend">Instance</span>
343
- <pre><code class="language-javascript">const stan = User({ email: "stan@smith.com" }); // no exceptions
344
- const roger = User({ name: "Roger" }); // email is mandatory</code>
345
- <code class="language-none exception">TypeError: expecting email to be String, got undefined</code></pre>
346
- </div>
347
- </section>
348
-
349
- <hr>
350
-
351
- <section id="doc-multiple-types" class="grid doc-code-code">
352
- <div class="doc">
353
- <h2>Multiple types</h2>
354
- <p>Several valid types can be specified for one property, aka <strong>union types</strong>.
355
- So optional properties are actually union types between the original type and the values
356
- <code>undefined</code> and <code>null</code>. To declare an optional union type, add
357
- <code>undefined</code> to the list.</p>
358
- </div>
359
-
360
- <div class="panel panel1">
361
- <span class="legend">Model</span>
362
- <pre><code class="language-javascript">const Animation = new ObjectModel({
363
- // can be a Number or a String
364
- delay: [Number, String],
365
-
366
- // optional property which can be a Boolean or a String
367
- easing: [Boolean, String, undefined]
368
- });</code></pre>
369
- </div>
370
-
371
- <div class="panel panel2">
372
- <span class="legend">Instance</span>
373
- <pre><code class="language-javascript">const opening = new Animation({ delay: 300 }); // easing is optional
374
- opening.delay = "fast"; // String is a valid type
375
- opening.delay = null;</code>
376
- <code class="language-none exception">TypeError: expecting delay to be Number or String, got null</code>
377
- <code class="language-javascript">opening.easing = true; // Boolean is a valid type
378
- opening.easing = 1;</code>
379
- <code class="language-none exception">TypeError: expecting easing to be Boolean or String or undefined, got Number 1</code></pre>
380
- </div>
381
- </section>
382
-
383
- <hr>
384
-
385
- <section id="doc-value-checking" class="grid doc-code">
386
- <div class="doc">
387
- <h2>Value checking and enumerations</h2>
388
- <p>In model definitions, you can also specify values instead of types for model properties. The property
389
- value will have to match the model one. Just like union types, use brackets notation for value
390
- enumerations.
391
- </p>
392
- <p>If a regular expression is passed, the value must match it.</p>
393
- </div>
394
-
395
- <div class="panel">
396
- <span class="legend">Model</span>
397
- <pre><code class="language-javascript">const Shirt = new ObjectModel({
398
- // the only acceptable value is "clothes"
399
- category: "clothes",
400
-
401
- // valid values: 38, 42, "S", "M", "L", "XL", "XXL"...
402
- size: [Number, "M", /^X{0,2}[SL]$/],
403
-
404
- // valid values: "black", "#FF0000", undefined...
405
- color: ["black","white", new RegExp("^#([A-F0-9]{6})$"), undefined]
406
- });</code></pre>
407
- </div>
408
- </section>
409
-
410
- <hr>
411
-
412
- <section id="doc-null-safe" class="grid doc-code-code">
413
- <div class="doc">
414
- <h2>Null-safe object traversal</h2>
415
- <p>When you want to traverse nested objects, you always have to worry about the null pointer exception.
416
- Some languages such as Groovy have a safe navigation operator represented by <code>?.</code> to
417
- safely navigate through potential null references. In JavaScript, there is no such solution so you
418
- have to manually check for <code>undefined/null</code> values at each level of the object. But
419
- within an object model, declared properties are null-safe for traversal:
420
- every instance complete its structure with undefined properties according to the model definition.
421
- </p>
422
- </div>
423
-
424
- <div class="panel panel1">
425
- <span class="legend">Model and instanciation</span>
426
- <pre><code class="language-javascript">const Config = new ObjectModel({
427
- local: {
428
- time: {
429
- format: ["12h","24h", undefined]
430
- }
431
- }
432
- });
433
-
434
- const config = { local: undefined };
435
- const new_config = Config(config); // object model</code></pre>
436
- </div>
437
-
438
- <div class="panel panel2">
439
- <span class="legend">Traversal</span>
440
- <pre><code class="language-javascript">if(config.local.time.format === "12h"){ hour %= 12; }</code>
441
- <code class="language-none exception">TypeError: Cannot read property 'time' of undefined</code>
442
-
443
- <code class="language-javascript">// so to prevent this exception, we have to check this way:
444
- if(config != null
445
- && config.local != null
446
- && config.local.time != null
447
- && config.local.time.format === "12h"){
448
- hour %= 12;
449
- }
450
-
451
- // with object models, no worries :)
452
- if(new_config.local.time.format === "12h"){ hour %= 12; }
453
- // new_config.local.time.format returns undefined</code></pre>
454
- </div>
455
- </section>
456
-
457
- <hr>
458
-
459
- <section id="doc-default-values" class="grid doc-code-code">
460
- <div class="doc">
461
- <h2>Default values assignment</h2>
462
- <p>You can set a default value for any model with <code>model.defaultTo(value)</code>. This default
463
- value will be used if no argument is passed to the model constructor.</p>
464
- </div>
465
-
466
- <div class="panel panel1">
467
- <span class="legend">Model</span>
468
- <pre><code class="language-javascript">let N = BasicModel(Number).defaultTo(1)</code></pre>
469
- </div>
470
-
471
- <div class="panel panel2">
472
- <span class="legend">Instance</span>
473
- <pre><code class="language-javascript">N(5) + N() === 6</code></pre>
474
- </div>
475
-
476
- <div class="doc">
477
- <p>For object models, the <code>defaultTo</code> method can be used to specify default values for some
478
- properties of your object models. If these are not defined at object instanciation, their default
479
- value will be assigned. You can also put them in the model prototype if you prefer to rely on
480
- <a rel="noopener" target="_blank"
481
- href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain">prototypal
482
- inheritance</a>.</p>
483
- </div>
484
-
485
- <div class="panel panel1">
486
- <span class="legend">Model</span>
487
- <pre><code class="language-javascript">const FileInfo = ObjectModel({
488
- name: String,
489
- size: [Number],
490
- creationDate: [Date],
491
- writable: Boolean
492
- }).defaultTo({
493
- name: "Untitled file",
494
- size: 0,
495
- writable: true
496
- });</code></pre>
497
- </div>
498
-
499
- <div class="panel panel2">
500
- <span class="legend">Instance</span>
501
- <pre><code class="language-javascript">let file = new FileInfo({ writable: false });</code>
502
- <code class="language-javascript">file.name; // name is mandatory but a default value was passed</code>
503
- <code class="language-none log">"Untitled file"</code>
504
- <code class="language-javascript">file.size; // size is optional, but the default value still applies</code>
505
- <code class="language-none log">0</code>
506
- <code class="language-javascript">file.creationDate; // no default value was passed for this property</code>
507
- <code class="language-none log">undefined</code>
508
- <code class="language-javascript">file.writable; // passed value overrides default value</code>
509
- <code class="language-none log">false</code>
510
- <code class="language-javascript">Object.keys(file);</code>
511
- <code class="language-none log">["name","size","creationDate","writable"]</code></pre>
512
- </div>
513
- </section>
514
-
515
- <hr>
516
-
517
- <section id="doc-composition" class="grid doc-code-code">
518
- <div class="doc">
519
- <h2>Composition with models as types</h2>
520
- <p>Nested properties definitions can be models too, so you can compose structures of models.</p>
521
- <p>When a property value matches a model definition, the value is automatically replaced by an instance
522
- of the corresponding model. This mechanic is referred as <strong>autocasting</strong> and can be
523
- compared to <a href="https://en.wikipedia.org/wiki/Duck_typing" target="_blank" rel="noopener"
524
- class="no-hl">
525
- duck typing
526
- </a>.
527
- Autocasting works for object models properties, but also for Array/Map/Set models items when
528
- inserted, and for FunctionModel arguments and return value.</p>
529
- <p>This naive approach is very time saving and allows you, for example, to parse composed models from
530
- JSON in one step. If there is somehow an ambiguity (such as two suitable models within an union
531
- type), the value is kept unchanged and a warning console message will inform you how to solve this
532
- ambiguity.</p>
533
- </div>
534
-
535
- <div class="panel panel1">
536
- <span class="legend">Model</span>
537
- <pre><code class="language-javascript">const Person = ObjectModel({
538
- name: String,
539
- age: [Number]
540
- });
541
-
542
- const Lovers = ObjectModel({
543
- husband: Person,
544
- wife: Person
545
- });</code></pre>
546
- </div>
547
-
548
- <div class="panel panel2">
549
- <span class="legend">Instance</span>
550
- <pre><code class="language-javascript">const joe = { name: "Joe", age: 42 };
551
- const ann = new Person({
552
- name: joe.name + "'s wife",
553
- age: joe.age - 5
554
- });
555
-
556
- const couple = Lovers({
557
- husband: joe, // object autocasted
558
- wife: ann // object model
559
- });
560
-
561
- couple.husband instanceof Person === true // has been casted to Person</code></pre>
562
- </div>
563
- </section>
564
-
565
- <hr>
566
-
567
- <section id="doc-extensions" class="grid doc-code-code">
568
- <div class="doc">
569
- <h2>Inheritance by extension</h2>
570
- <p>Extensions create new models based on existing model definitions. You can declare new properties or
571
- override previous ones. Therefore, it is an easy way to reproduce subtyping and class inheritance
572
- patterns.</p>
573
- </div>
574
-
575
- <div class="panel panel1">
576
- <span class="legend">Model</span>
577
- <pre><code class="language-javascript">const Person = ObjectModel({
578
- name: String,
579
- female: Boolean
580
- });
581
-
582
- const Mother = Person.extend({
583
- female: true,
584
- child: Person
585
- });</code></pre>
586
- </div>
587
-
588
- <div class="panel panel2">
589
- <span class="legend">Instance</span>
590
- <pre><code class="language-javascript">let joe = new Person({ name: "Joe", female: false });
591
- let ann = new Person({ name: "Ann", female: true });
592
- let joanna = new Person({ name: "Joanna", female: true });
593
-
594
- ann = new Mother({ name: "Ann", female: true, child: joanna })
595
- ann instanceof Mother && ann instanceof Person // true</code>
596
-
597
- <code class="language-javascript">joe = Mother(joe); // try to cast joe to Mother model</code>
598
- <code class="language-none exception">TypeError: expecting female to be true, got Boolean false
599
- expecting child to be {
600
- name: String,
601
- female: Boolean
602
- }, got undefined</code></pre>
603
- </div>
604
-
605
- <div class="doc">
606
- <h3>With ES6 classes</h3>
607
- <p>Extended models inherit the parent's prototype chain, so you can easily combine it with class
608
- inheritance. Just make sure to respect the <a
609
- href="https://en.wikipedia.org/wiki/Liskov_substitution_principle" target="_blank"
610
- rel="noopener">Liskov substitution principle</a>
611
- when you extend a type definition.</p>
612
- </div>
613
-
614
- <div class="panel panel1">
615
- <span class="legend">Model</span>
616
- <pre><code class="language-javascript">class Person extends ObjectModel({ name: String, female: Boolean }){
617
- constructor({ name, female }){
618
- if(!female) name = `Mr ${name}`
619
- super({ name, female })
620
- }
621
- }
622
-
623
- class Mother extends Person.extend({ female: true, child: Person }){
624
- constructor({ name, female, child }){
625
- super({ name: `Mrs ${name}`, female, child })
626
- }
627
- }</code></pre>
628
- </div>
629
-
630
- <div class="panel panel2">
631
- <span class="legend">Instance</span>
632
- <pre><code class="language-javascript">let joe = new Person({ name: "Joe", female: false })
633
- let joanna = new Person({ name: "Joanna", female: true })
634
- let ann = new Mother({ name: "Ann", female: true, child: joanna })</code>
635
-
636
- <code class="language-javascript">joe.name</code>
637
- <code class="language-none log">Mr Joe</code>
638
- <code class="language-javascript">ann.name</code>
639
- <code class="language-none log">Mrs Ann</code>
640
- <code class="language-javascript">ann.child.name</code>
641
- <code class="language-none log">Joanna</code></pre>
642
- </div>
643
-
644
- </section>
645
-
646
- <hr>
647
-
648
- <section id="doc-multiple-inheritance" class="grid doc-code-code">
649
- <div class="doc">
650
- <h2>Multiple inheritance</h2>
651
- <p>But it goes further: you can do multiple inheritance and mix any number of parent models definitions
652
- and assertions. If some properties have the same name, those of the last object overrides the
653
- others.</p>
654
- </div>
655
-
656
- <div class="panel panel1">
657
- <span class="legend">Model</span>
658
- <pre><code class="language-javascript">const Client = Person.extend(User, Order, { store: String });
659
-
660
- Client.prototype.sendConfirmationMail = function(){
661
- return this.email + ": Dear " + this.name
662
- + ", thank you for ordering "
663
- + this.product.quantity + " " + this.product.name
664
- + " on " + this.store;
665
- };
666
-
667
- Object.keys(Client.definition);</code>
668
- <code class="language-none log">["name", "female", "email", "product", "orderDate", "store"]</code></pre>
669
- </div>
670
-
671
- <div class="panel panel2">
672
- <span class="legend">Instance</span>
673
- <pre><code class="language-javascript">const joe = new Client({
674
- name: "Joe",
675
- female: false,
676
- email: "joe@email.net",
677
- product: { name: "diapers", quantity: 100 },
678
- orderDate: new Date(),
679
- store: "daddy.net"
680
- });
681
-
682
- joe.sendConfirmationMail();</code>
683
- <code class="language-none log">joe@email.net: Dear Joe, thank you for ordering 100 diapers on daddy.net</code></pre>
684
- </div>
685
- </section>
686
-
687
- <hr>
688
-
689
- <section id="doc-assertions" class="grid doc-code-code">
690
- <div class="doc">
691
- <h2>Assertions for custom validation tests</h2>
692
- <p>You can add to your models any number of assertions that are custom test functions applied on model
693
- instances. All assertions are called every time the model is changed, and must all return
694
- <code>true</code> to validate. Exceptions thrown in assertions are catched and considered as
695
- assertion failures.</p>
696
- <p>For example, we can get an Integer model by adding <code>Number.isInteger</code> as an assertion to a
697
- basic <code>Number</code> model.</p>
698
- <p>Assertions are inherited from the model prototype, so you can add global assertions on all models by
699
- setting them in <code>Model.prototype</code>. The second argument of the <code>assert</code> method
700
- is an optional message shown when assertion fails. It can be a String or a function returning a
701
- String.</p>
702
- </div>
703
-
704
- <div class="panel panel1">
705
- <span class="legend">Model</span>
706
- <pre><code class="language-javascript">const PositiveInteger = BasicModel(Number)
707
- .assert(Number.isInteger)
708
- .assert(n => n >= 0, "should be greater or equal to zero")
709
-
710
- function isPrime(n) {
711
- for (let i=2, m=Math.sqrt(n); i <= m ; i++){
712
- if(n%i === 0) return false;
713
- }
714
- return n > 1;
715
- }
716
-
717
- const PrimeNumber = PositiveInteger.extend().assert(isPrime);
718
- // extend to not add isPrime assertion to the Integer model
719
- </code></pre>
720
- </div>
721
-
722
- <div class="panel panel2">
723
- <span class="legend">Instance</span>
724
- <pre><code class="language-javascript">PositiveInteger(-1);</code>
725
- <code class="language-none exception">TypeError: assertion should be greater or equal to zero returned false for value -1</code>
726
-
727
- <code class="language-javascript">PositiveInteger(Math.sqrt(2));</code>
728
- <code class="language-none exception">TypeError: assertion isInteger returned false for value 1.414213562373</code>
729
-
730
- <code class="language-javascript">PrimeNumber(83);</code>
731
- <code class="language-none log">83</code>
732
-
733
- <code class="language-javascript">PrimeNumber(87);</code>
734
- <code class="language-none exception">TypeError: assertion isPrime returned false for value 87</code></pre>
735
- </div>
736
- </section>
737
-
738
- <hr>
739
-
740
- <section id="doc-private-and-constants" class="grid doc-code-code">
741
- <div class="doc">
742
- <h2>Private and constant properties</h2>
743
- <p>Some variable naming conventions are commonly used in JavaScript. For example, a leading underscore
744
- is used to specify a <i>_private</i> property which should not be used outside the object own
745
- methods. Also, constants are often in <i>ALL_CAPS</i>. Model definitions follow these conventions by
746
- making <i>_underscored</i> properties not enumerable and not usable outside of the instance's own
747
- methods, and <i>CAPITALIZED</i> properties not writable.</p>
748
- <p>Note: private properties access is granted only when using the instance own methods. Methods declared
749
- in an extended class cannot access to privates. Asynchronous callbacks do not work neither, except
750
- if these callbacks are defined as methods of the model. If this does not fit your usecase, you
751
- should probably not make these properties private.</p>
752
- </div>
753
-
754
- <div class="panel panel1">
755
- <span class="legend">Model</span>
756
- <pre><code class="language-javascript">const Circle = ObjectModel({
757
- radius: Number, // public
758
- _index: Number, // private
759
- UNIT: ["px","cm"], // constant
760
- _ID: [Number], // private and constant
761
- }).defaultTo({
762
- _index: 0,
763
- getIndex(){ return this._index },
764
- setIndex(value){ this._index = value }
765
- });
766
- </code></pre>
767
- </div>
768
-
769
- <div class="panel panel2">
770
- <span class="legend">Instance</span>
771
- <pre><code class="language-javascript">let c = new Circle({ radius: 120, UNIT: "px", _ID: 1 });
772
- c.radius = 100;
773
- c.UNIT = "cm";</code>
774
- <code class="language-none exception">TypeError: cannot modify constant property UNIT</code>
775
-
776
- <code class="language-javascript">c._index = 1;</code>
777
- <code class="language-none exception">TypeError: cannot modify private property _index</code>
778
- <code class="language-javascript">console.log( c._index )</code>
779
- <code class="language-none exception">TypeError: cannot access to private property _index</code>
780
- <code class="language-javascript">c.setIndex(2);
781
- console.log( c.getIndex() )</code>
782
- <code class="language-none log">2</code>
783
- <code class="language-javascript">Object.keys(c); // private variables are not enumerated</code>
784
- <code class="language-none log">["radius", "UNIT"]</code></pre>
785
- </div>
786
-
787
- <div class="doc">
788
- <p>You can modify or remove these conventions by overriding the
789
- <code>conventionForPrivate</code> and
790
- <code>conventionForConstant</code> methods in your model or globally in
791
- <code>Model.prototype</code>.
792
- </p>
793
- </div>
794
-
795
- <div class="panel panel1">
796
- <pre><code class="language-javascript">// change the private convention for all models
797
- Model.prototype.conventionForPrivate = key => key.startsWith('#');
798
-
799
- // remove the constant convention specifically for Circle
800
- Circle.conventionForConstant = () => false;</code></pre>
801
- </div>
802
-
803
- <div class="panel panel2">
804
- <pre><code class="language-javascript">// Private and constant conventions have been changed
805
- c._index = 3;
806
- c.UNIT = "cm";
807
-
808
- console.log(c._index, c.UNIT); // no more errors</code>
809
- <code class="language-none log">3 "cm"</code></pre>
810
- </div>
811
- </section>
812
- <hr>
813
-
814
- <section id="doc-array-model" class="grid doc-code-code">
815
- <div class="doc">
816
- <h2>Array models</h2>
817
- <p>Array models validate the type of all elements in an array.</p>
818
- <p>The validation is done on initial array elements passed to the model, then on new elements added or
819
- modified afterwards.</p>
820
- </div>
821
-
822
- <div class="panel panel1">
823
- <span class="legend">Model</span>
824
- <pre><code class="language-javascript">import { ArrayModel } from "objectmodel";
825
-
826
- const Cards = new ArrayModel([Number, "J","Q","K"]);
827
-
828
- // Hand is an array of 2 Numbers, J, Q, or K
829
- const Hand = Cards.extend()
830
- .assert(a => a.length === 2, "should have two cards");</code></pre>
831
- </div>
832
-
833
- <div class="panel panel2">
834
- <span class="legend">Instance</span>
835
- <pre><code class="language-javascript">const myHand = Hand( [7, "K"] );
836
- myHand[0] = "Joker"</code>
837
- <code class="language-none exception">TypeError: expecting Array[0] to be Number or "J" or "Q" or "K", got String "Joker"</code>
838
- <code class="language-javascript">myHand.push("K");</code>
839
- <code class="language-none exception">TypeError: assertion "should have two cards" returned false for value [7, "Joker", "K"]</code></pre>
840
- </div>
841
-
842
- <div class="doc">
843
- <p>All the validation options for previous models are also available for array model elements:
844
- type/value checking, optional properties, union types, enumerations, assertions...</p>
845
- </div>
846
-
847
- <div class="panel panel1">
848
- <span class="legend">Model</span>
849
- <pre><code class="language-javascript">const Family = ObjectModel({
850
- father: Father,
851
- mother: Mother,
852
- children: ArrayModel(Person), // array of Persons
853
- grandparents: [ArrayModel([Mother, Father])]
854
- // optional array of Mothers or Fathers
855
- });</code></pre>
856
- </div>
857
-
858
- <div class="panel panel2">
859
- <span class="legend">Instance</span>
860
- <pre><code class="language-javascript">const joefamily = new Family({
861
- father: joe,
862
- mother: ann,
863
- children: [joanna, "dog"]
864
- });</code>
865
- <code class="language-none exception">TypeError: expecting Array[1] to be { name: String, female: Boolean }, got String "dog"</code>
866
- </pre>
867
- </div>
868
- </section>
869
-
870
- <hr>
871
-
872
- <section id="doc-function-model" class="grid doc-code-code">
873
- <div class="doc">
874
- <h2>Function models</h2>
875
- <p>Function models provide validation on input (arguments) and output (return value). All the validation
876
- options for Object models are also available for Function models. The arguments passed to
877
- <code>FunctionModel</code> are the types of the arguments the function will receive, and the
878
- <code>return</code> method is used to specify the type of the function return value.</p>
879
- </div>
880
-
881
- <div class="panel panel1">
882
- <pre><code class="language-javascript">import { FunctionModel, BasicModel } from "objectmodel";
883
-
884
- const Numb = BasicModel(Number).assert(Number.isFinite);
885
- const Operator = BasicModel(["+","-","*","/"])
886
-
887
- const Calculator = FunctionModel(Numb, Operator, Numb).return(Numb);
888
-
889
- const calc = new Calculator((a, operator, b) => eval(a + operator + b));
890
- </code></pre>
891
- </div>
892
-
893
- <div class="panel panel2">
894
- <pre><code class="language-javascript">calc(3, "+", 1);</code>
895
- <code class="language-none log">4</code>
896
- <code class="language-javascript">calc(6, "*", null);</code>
897
- <code class="language-none exception">TypeError: expecting arguments[2] to be Number, got null</code>
898
- <code class="language-javascript">calc(1, "/", 0);</code>
899
- <code class="language-none exception">TypeError: assertion "isFinite" returned false for value Infinity</code></pre>
900
- </div>
901
-
902
- <div class="doc">
903
- <p>In classical JavaScript OOP programming, methods are declared in the constructor's
904
- <code>prototype</code>. You can do the same with instances of function models.</p>
905
- <p>Another option is to provide a default implementation in the model definition by using the
906
- <code>defaultTo</code> method. See the <a href="#doc-default-values">Default values</a> section.
907
- The difference is that all the properties in the model definition are required for an object
908
- to be considered suitable for the model. In the following example, an object must have a function
909
- <code>sayMyName</code> to be valid as a Person, while the function <code>greet</code> is not
910
- mandatory.</p>
911
- </div>
912
-
913
- <div class="panel panel1">
914
- <span class="legend">Model</span>
915
- <pre><code class="language-javascript">const Person = ObjectModel({
916
- name: String,
917
- // function without arguments returning a String
918
- sayMyName: FunctionModel().return(String)
919
- }).defaultTo({
920
- sayMyName: function(){ return "my name is " + this.name }
921
- })
922
-
923
- // takes one Person as argument, returns a String
924
- Person.prototype.greet = FunctionModel(Person).return(String)(
925
- function(otherguy){
926
- return "Hello "+ otherguy.name + ", " + this.sayMyName()
927
- }
928
- )</code></pre>
929
- </div>
930
-
931
- <div class="panel panel2">
932
- <span class="legend">Instance</span>
933
- <pre><code class="language-javascript">const joe = new Person({ name: "Joe" });
934
-
935
- joe.sayMyName();</code>
936
- <code class="language-none log">my name is Joe</code>
937
- <code class="language-javascript">joe.greet({ name: "Ann", greet: "hi ?" });</code>
938
- <code class="language-none log">Hello Ann, my name is Joe</code>
939
- <code class="language-javascript">joe.greet({ name: "dog", sayMyName: "woof !" });</code>
940
- <code class="language-none exception">TypeError: expecting arguments[0].sayMyName to be "Function", got String "woof !"</code></pre>
941
- </div>
942
-
943
- </section>
944
-
945
- <hr>
946
-
947
- <section id="doc-map-models" class="grid doc-code-code">
948
- <div class="doc">
949
- <h2>Map models</h2>
950
- <p>Map models validate ES6 <code>Map</code> objects by checking both keys and values. The arguments
951
- passed to <code>MapModel</code> are respectively the definition for the keys and the definition for
952
- the values.</p>
953
- </div>
954
-
955
- <div class="panel panel1">
956
- <span class="legend">Model</span>
957
- <pre><code class="language-javascript">import { MapModel, Model } from "objectmodel";
958
-
959
- const Course = Model([ "math", "english", "history" ])
960
- const Grade = Model([ "A", "B", "C" ])
961
-
962
- const Gradebook = MapModel(Course, Grade)
963
- </code></pre>
964
- </div>
965
-
966
- <div class="panel panel2">
967
- <span class="legend">Instance</span>
968
- <pre><code class="language-javascript">const joannaGrades = new Gradebook([
969
- ["math", "B"],
970
- ["english", "C"]
971
- ])
972
-
973
- joannaGrades.set("videogames", "A")</code>
974
- <code class="language-none exception">TypeError: expecting Map key to be "math" or "english" or "history", got String "videogames"</code>
975
- <code class="language-javascript">joannaGrades.set("history", "nope")</code>
976
- <code class="language-none exception">TypeError: expecting Map["history"] to be "A" or "B" or "C" , got String "nope"</code></pre>
977
- </div>
978
- </section>
979
-
980
- <hr>
981
-
982
- <section id="doc-set-models" class="grid doc-code-code">
983
- <div class="doc">
984
- <h2>Set models</h2>
985
- <p>Set models validate ES6 <code>Set</code> objects by checking the type of all the elements in the set.
986
- The API is the same as array models.</p>
987
- </div>
988
-
989
- <div class="panel panel1">
990
- <span class="legend">Model</span>
991
- <pre><code class="language-javascript">import { SetModel, Model } from "objectmodel";
992
-
993
- const Course = Model([ "math", "english", "history" ])
994
-
995
- const FavoriteCourses = SetModel(Course)
996
- </code></pre>
997
- </div>
998
-
999
- <div class="panel panel2">
1000
- <span class="legend">Instance</span>
1001
- <pre><code class="language-javascript">const joannaFavorites = FavoriteCourses([ "math", "english" ])
1002
-
1003
- joannaFavorites.add("sleeping")</code>
1004
- <code class="language-none exception">TypeError: expecting Set value to be "math" or "english" or "history", got String "sleeping"</code></pre>
1005
- </div>
1006
- </section>
1007
-
1008
- <hr>
1009
-
1010
- <section id="doc-any-model" class="grid doc-code">
1011
- <div class="doc">
1012
- <h2>Any model</h2>
1013
- <p>The <code>Any</code> model is used to define a property or parameter that can take <i>any</i> value.
1014
- It is better than an union type with all primitives and objects, as it skips every validation step
1015
- instead of checking every possible type.</p>
1016
- </div>
1017
-
1018
- <div class="panel">
1019
- <pre><code class="language-javascript">import { Any, ObjectModel, ArrayModel, FunctionModel } from "objectmodel";
1020
-
1021
- // examples using the Any Model
1022
- const DataWrapper = ObjectModel({ data: Any })
1023
- const ArrayNotEmpty = ArrayModel(Any).assert(arr => arr.length > 0)
1024
- const Serializer = FunctionModel(Any).return(String);</code></pre>
1025
- </div>
1026
- </section>
1027
-
1028
- <section id="doc-any-remainining" class="grid doc-code">
1029
- <div class="doc">
1030
- <h2>...Any remaining parameter</h2>
1031
- <p>The <code>Any</code> model can also be used as <code>...Any</code> in <code>FunctionModel</code>
1032
- parameters definition to specify a function that can take <i>any</i> amount of parameters.
1033
- <code>...Any</code> is a straight-forward syntax for functions already using the ES6
1034
- <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters"
1035
- target="_blank" rel="noopener">rest operator</a>, but you can use <code>Any.remaining</code> as
1036
- an alias to
1037
- <code>...Any</code> if you prefer.</p>
1038
- <p>The remaining parameters can be checked against a definition passed as argument
1039
- (<code>...Any(def)</code> or <code>Any.remaining(def)</code>). That definition defaults to
1040
- <code>Any</code> if not specified.</p>
1041
- </div>
1042
-
1043
- <div class="panel">
1044
- <pre><code class="language-javascript">import { Any, FunctionModel } from "objectmodel";
1045
-
1046
- // takes 2 parameters or more
1047
- const Operation = FunctionModel(Number, Number, ...Any)
1048
-
1049
- // takes any amount of Numbers as parameters
1050
- const NumericOperation = FunctionModel(...Any(Number)).return(Number)</code></pre>
1051
- </div>
1052
- </section>
1053
-
1054
- <hr>
1055
-
1056
- <section id="doc-custom-collectors" class="grid doc-code-code">
1057
- <h2>Custom error collectors</h2>
1058
- <div class="doc">
1059
- <p>By default, validation errors are collected every time a model instance is created or modified, and
1060
- thrown as <code>TypeError</code> exceptions with a message describing all the errors found. It it
1061
- possible to change this behaviour and add your own error collectors. For example, you may want to
1062
- notify the user that an error occurred, or send the information to your server for error tracking on
1063
- production.</p>
1064
- </div>
1065
- <div class="doc">
1066
- <p>Error collectors are callback functions called with an array of all the errors collected during the
1067
- last model inspection. Every error is an object with these properties:</p>
1068
- <ul>
1069
- <li><code>message</code>: a message describing the error</li>
1070
- <li><code>expected</code>: the expected type definition or assertion/li>
1071
- <li><code>received</code>: the received value, to compare to the expected</li>
1072
- <li><code>path</code>: the path where the error occurred in an object model definition</li>
1073
- </ul>
1074
- </div>
1075
-
1076
- <div class="doc">
1077
- <h3>Global error collector</h3>
1078
- <p>This is how you define an error collector globally for all models.</p>
1079
- </div>
1080
-
1081
- <div class="panel panel1">
1082
- <pre><code class="language-javascript">Model.prototype.errorCollector = function(errors){
1083
- console.log("Global error collector caught these errors:");
1084
- errors.forEach(error => { console.dir(error) });
1085
- };
1086
-
1087
- const Student = ObjectModel({
1088
- name: String,
1089
- course: [ "math","english","history" ],
1090
- grade: Number
1091
- }).assert(student => student.grade >= 60,
1092
- "should at least get 60 to validate semester")
1093
-
1094
- new Student({ name: "Joanna", course: "sleep", grade: 0 });</code></pre>
1095
- </div>
1096
-
1097
- <div class="panel panel2">
1098
- <span class="legend">Result</span>
1099
- <pre><code class="language-none log">Global error collector caught these errors:
1100
- {
1101
- message: 'expecting course to be "math" or "english" or "history", got String "sleep"'
1102
- path: "course"
1103
- expected: ["math","english","history"]
1104
- received: "sleep"
1105
- }
1106
- {
1107
- message: "assertion should at least get 60 to validate semester returned false for value { name: "Joanna", course: "sleep", grade: 0 }",
1108
- path: null,
1109
- expected: student => student.grade >= 60,
1110
- received: { name: "Joanna", course: "sleep", grade: 0 }
1111
- }</code></pre>
1112
- </div>
1113
-
1114
- <div class="doc">
1115
- <h3>Model error collector</h3>
1116
- <p>This is how you define an error collector specifically by model</p>
1117
- </div>
1118
-
1119
- <div class="panel panel1">
1120
- <pre><code class="language-javascript">Student.errorCollector = function(errors){
1121
- console.log("Student model error collector caught these errors:");
1122
- errors.forEach(error => { console.dir(error) });
1123
- };
1124
-
1125
- new Student({ name: "Joanna", course: "math", grade: 50 });</code></pre>
1126
- </div>
1127
-
1128
- <div class="panel panel2">
1129
- <span class="legend">Result</span>
1130
- <pre><code class="language-none log">Student model collector caught these errors:
1131
- {
1132
- message: "assertion should at least get 60 to validate semester returned false for value { name: "Joanna", course: "math", grade: 50 }",
1133
- path: null,
1134
- expected: student => student.grade >= 60,
1135
- received: { name: "Joanna", course: "math", grade: 50 }
1136
- }</code></pre>
1137
- </div>
1138
-
1139
- <div class="doc">
1140
- <h3>Single-use error collector</h3>
1141
- <p>And this is how you define an error collector to be used only once with
1142
- <code>test(obj, myErrorCollector)</code></p>
1143
- </div>
1144
-
1145
- <div class="panel panel1">
1146
- <pre><code class="language-javascript">Student.test({
1147
- name: "Joanna",
1148
- course: "cheating",
1149
- grade: 90
1150
- }, function(errors){
1151
- console.log("This specific error collector caught these errors:");
1152
- errors.forEach(error => { console.dir(error) });
1153
- });</code></pre>
1154
- </div>
1155
-
1156
- <div class="panel panel2">
1157
- <span class="legend">Result</span>
1158
- <pre><code class="language-none log">This specific error collector caught these errors:
1159
- {
1160
- message: 'expecting course to be "math" or "english" or "history", got String "cheating"'
1161
- path: "course"
1162
- expected: ["math","english","history","science"]
1163
- received: "cheating"
1164
- }</code></pre>
1165
- </div>
1166
-
1167
- </section>
1168
-
1169
- <hr>
1170
-
1171
- <section id="doc-custom-devtool-formatters">
1172
- <h2>Custom devtool formatters</h2>
1173
-
1174
- <p>ObjectModel provides custom formatters for Models and Model instances in Chrome Developer Tools,
1175
- available in Chrome and Opera. These formatters improve the way models and instances are displayed when
1176
- logged in the console.</p>
1177
-
1178
- <h3>Enabling custom formatters</h3>
1179
-
1180
- <p>Chrome currently doesn’t have custom formatters enabled by default. You need to enter the DevTools
1181
- settings via the menu at the top right of the DevTools panel, then select <i>Preferences</i> and check
1182
- <i>Enable custom formatters</i> in the <i>Console</i> section.</p>
1183
-
1184
- <div>
1185
- <figure style="display: inline-block; vertical-align: top">
1186
- <img class="lazy" alt="Enabling custom formatters in Chrome"
1187
- data-src="docs/res/custom-formatters-chrome-settings.jpg">
1188
- </figure>
1189
-
1190
- <figure style="display: inline-block; vertical-align: top">
1191
- <img class="lazy" alt="Screenshot without custom formatters"
1192
- data-src="docs/res/custom-formatters-before.jpg">
1193
- <img class="lazy" alt="Screenshot with custom formatters"
1194
- data-src="docs/res/custom-formatters-after.jpg">
1195
- </figure>
1196
- </div>
1197
-
1198
- <p>The formatters for ObjectModel are <strong>included by default in the unminified bundle
1199
- (<code>dist/object-model.js</code>)</strong>.
1200
- If you are using modules, import them manually with :</p>
1201
- <pre><code class="language-javascript">import * from "objectmodel/src/devtool-formatter"</code></pre>
1202
-
1203
- <h3>Specifying the Model Name <small>(since v3.4)</small></h3>
1204
- <p>A variable name is irrelevant to name a Model, because several variables with different names can point
1205
- to the same Model reference. To specify a unique model name for debugging purposes, you can use the
1206
- <code>as()</code> method like this :</p>
1207
- <pre><code class="language-javascript">const Integer = Model(Number).assert(Number.isInteger).as("Integer");
1208
- console.log(Integer.name) // "Integer"</code></pre>
1209
- </section>
1210
-
1211
- <hr>
1212
-
1213
- <section id="api">
1214
- <h2>Full API</h2>
1215
- <h3>Imported from objectmodel root scope</h3>
1216
- <dl>
1217
- <dt><a href="#doc-model">Model</a> <code>Model(definition)</code></dt>
1218
- <dd>Constructor alias for basic and object models</dd>
1219
-
1220
- <dt><a href="#doc-basic-model">BasicModel</a> <code>BasicModel(definition)</code></dt>
1221
- <dd>Constructor for basic models</dd>
1222
-
1223
- <dt><a href="#doc-object-model">ObjectModel</a> <code>ObjectModel(definition)</code></dt>
1224
- <dd>Constructor for object models</dd>
1225
-
1226
- <dt><a href="#doc-array-model">ArrayModel</a> <code>ArrayModel(itemDefinition)</code></dt>
1227
- <dd>Constructor for array models</dd>
1228
-
1229
- <dt><a href="#doc-function-model">FunctionModel</a> <code>FunctionModel(definitionArgument1,
1230
- definitionArgument2, ...)</code></dt>
1231
- <dd>Constructor for function models</dd>
1232
-
1233
- <dt><a href="#doc-map-model">MapModel</a> <code>MapModel(keyDefinition, valueDefinition)</code></dt>
1234
- <dd>Constructor for map models</dd>
1235
-
1236
- <dt><a href="#doc-set-model">SetModel</a> <code>SetModel(itemDefinition)</code></dt>
1237
- <dd>Constructor for set models</dd>
1238
-
1239
- </dl>
1240
- <h3>Model methods and properties</h3>
1241
- <dl>
1242
- <dt>name <code>model.name</code></dt>
1243
- <dd>The name of the model, used for debugging purposes</dd>
1244
-
1245
- <dt>as <code>model.as(newName)</code></dt>
1246
- <dd>Set the name of the model</dd>
1247
-
1248
- <dt>definition <code>model.definition</code></dt>
1249
- <dd>Returns the model definition</dd>
1250
-
1251
- <dt><a href="#doc-extensions">extend</a> <code>model.extend(...otherModelsOrDefinitions)</code></dt>
1252
- <dd>Returns a new model based on the initial model merged with other definitions/assertions</dd>
1253
-
1254
- <dt>assertions <code>model.assertions</code></dt>
1255
- <dd>Returns the list of model assertions</dd>
1256
-
1257
- <dt><a href="#doc-assertions">assert</a> <code>model.assert(assertion, [description])</code></dt>
1258
- <dd>Add a test function to the model that must return <code>true</code> to validate the instance.</dd>
1259
-
1260
- <dt>default <code>model.default</code></dt>
1261
- <dd>Returns the default value if defined</dd>
1262
-
1263
- <dt><a href="#doc-default-values">defaultTo</a> <code>model.defaultTo(defaultValue)</code></dt>
1264
- <dd>Set the default value of the model</dd>
1265
-
1266
- <dt>errorCollector <code>model.errorCollector = function(errors){ ... }</code></dt>
1267
- <dd>Function called when validation errors are detected</dd>
1268
-
1269
- <dt><a href="#doc-composition">test</a> <code>model.test(value, [errorCollector])</code></dt>
1270
- <dd>Returns <code>true</code> if the value passed validates the model definition.
1271
- Works with <a href="#doc-composition">autocasting</a>.
1272
- A custom error collector can be specified to retrieve the validation errors.</dd>
1273
-
1274
- <dt><a href="#doc-private-and-constants">conventionForConstant</a> <code>function(variableName)</code>
1275
- </dt>
1276
- <dd>Internal function used to identify a constant property based on naming convention. You can override
1277
- it to suit your needs.</dd>
1278
-
1279
- <dt><a href="#doc-private-and-constants">conventionForPrivate</a> <code>function(variableName)</code>
1280
- </dt>
1281
- <dd>Internal function used to identify a non-enumerable property based on naming convention. You can
1282
- override it to suit your needs.</dd>
1283
-
1284
- </dl>
1285
- <h3>Object models</h3>
1286
- <dl>
1287
- <dt>defaultTo <code>objectModel.defaultTo(defaultValuesObject)</code></dt>
1288
- <dd>Set the default values for some model properties.</dd>
1289
- </dl>
1290
- <h3>Function models</h3>
1291
- <dl>
1292
- <dt>return <code>functionModel.return(returnValueDefinition)</code></dt>
1293
- <dd>Set the definition of the return value. Each call to the function must return a validated value,
1294
- otherwise an exception will be raised.</dd>
1295
- </dl>
1296
- </section>
1297
-
1298
- <hr>
1299
-
1300
- <section id="common-models">
1301
- <h2>Commonly used models</h2>
1302
- <p>Here are some models that you may find useful. <strong>These are not included in the library</strong>, so
1303
- pick what you need or <a href="docs/examples/common.js">get them all from here</a></p>
1304
-
1305
- <pre><code class="language-javascript" data-source="docs/examples/common.js"></code></pre>
1306
- </section>
1307
- <hr>
1308
-
1309
- <section id="common-questions">
1310
- <h2>Common questions</h2>
1311
-
1312
- <details>
1313
- <summary id="browser-support">Which browsers and node versions are supported ?</summary>
1314
- <p>This library is <a href="test/" target="_blank">unit tested</a> against these browsers and Node.js
1315
- versions, depending of the version of Object Model:</p>
1316
- <dl>
1317
- <dt><a href="docs/v1/">v1.x</a></dt>
1318
- <dd>Chrome 29+, Firefox 24+, Edge, Internet Explorer 9+, Opera 20+, Safari 5.1+, Node.js 4.0+</dd>
1319
- <dt><a href="docs/v2/">v2.x</a></dt>
1320
- <dd>Support for IE < 11 had to be dropped in v2 because it required many hacks and was holding back
1321
- other browsers. Otherwise, same support than v1.</dd> <dt><a href="docs/v3/">v3.x</a></dt>
1322
- <dd>ObjectModel v3 is built around ES6 Proxies, so requires modern environments :
1323
- Edge 14+, Firefox 47+, Chrome 50+, Safari 10+, Node 6.0+.</dd>
1324
- <dt>v4.x</dt>
1325
- <dd>Support for Node 6-7 has been dropped in v4. Use a transpiler like Babel to target the same
1326
- environments than v3.</dd>
1327
- </dl>
1328
- <p>More information about these changes between major versions on the
1329
- <a href="https://github.com/sylvainpolletvillard/ObjectModel/releases" target="_blank"
1330
- rel="noopener">
1331
- Github Releases</a> page</p>
1332
- </details>
1333
-
1334
- <details>
1335
- <summary>What is the impact on performance ?</summary>
1336
- <p>To get dynamic type validation, Object models have to use Proxies to catch properties assignments.
1337
- This has a performance cost, especially on older browsers. Therefore, it is not advisable to use
1338
- object models in performance-critical parts of your applications. In particular, Array models and
1339
- circular references in models have the most impact on performance. But in general, the loss of time
1340
- does not exceed a few milliseconds and is quite negligible.</p>
1341
- </details>
1342
-
1343
- <details>
1344
- <summary>How can I get the model from an instance ?</summary>
1345
- <div class="grid doc-code">
1346
- <div class="doc">
1347
- <p>With the <code>constructor</code> property. If this property is used in your model, you can
1348
- also retrieve it with <code>Object.getPrototypeOf(instance).constructor</code>. This is
1349
- useful for retrieving the type of a property for example.</p>
1350
- </div>
1351
- <div class="panel">
1352
- <pre><code class="language-javascript">const User = ObjectModel({ name: String }),
1353
- joe = User({ name: "Joe" });
1354
-
1355
- const modelOfJoe = joe.constructor // or Object.getPrototypeOf(joe).constructor;
1356
- // modelOfJoe === User
1357
- // modelOfJoe.definition.name === String</code></pre>
1358
- </div>
1359
- </div>
1360
- </details>
1361
-
1362
- <details>
1363
- <summary>
1364
- How do I declare a constructor function to be called on instanciation before validating my model ?
1365
- </summary>
1366
- <div class="grid doc-code">
1367
- <div class="doc">
1368
- <p>The recommended way is to use a factory function to instanciate your models. You can declare
1369
- as many different factories as needed, which makes this pattern both simple and flexible.
1370
- </p>
1371
- </div>
1372
- <div class="panel">
1373
- <pre><code class="language-javascript">const User = ObjectModel({
1374
- firstName: String,
1375
- lastName: String,
1376
- fullName: String
1377
- });
1378
-
1379
- User.create = function(properties){
1380
- properties.fullName = properties.firstName + " " + properties.lastName;
1381
- return new User(properties);
1382
- };
1383
-
1384
- const joe = User.create({ firstName: "Joe", lastName: "Dalton" });</code></pre>
1385
- </div>
1386
- </div>
1387
- </details>
1388
-
1389
- <details>
1390
- <summary>Is there a way to compute default values ?</summary>
1391
- <div class="grid doc-code">
1392
- <div class="doc">
1393
- <p>Computed default values are not really default values in the sense that these values are
1394
- different from one instance to another, and computed at instanciation time. On the other
1395
- hand, this computation logic is the same for all instances. So this computation logic should
1396
- be declared as a getter/setter in the model prototype.</p>
1397
- <p>One could have different expectations regarding whether these values are computed <i>once at instanciation</i> or <i>at every property read</i>. But this <i>once at instanciation</i> behaviour can also be implemented with a getter using another undeclared private property as cache, as shown in this code example.</p>
1398
- <p>See <a href="https://github.com/sylvainpolletvillard/ObjectModel/issues/107" target="_blank"
1399
- rel="external">issue #107</a> for more information.</p>
1400
- </div>
1401
- <div class="panel">
1402
- <pre><code class="language-javascript">const T = new ObjectModel({
1403
- id: String
1404
- });
1405
-
1406
- T.prototype = {
1407
- get id() {
1408
- if(this._id === undefined) this._id = generateUID()
1409
- return this._id
1410
- },
1411
- set id(newId){
1412
- this._id = newId
1413
- }
1414
- };</code></pre>
1415
- </div>
1416
- </div>
1417
- </details>
1418
-
1419
- <details>
1420
- <summary>How do I prevent adding undeclared properties to my object models ?</summary>
1421
- <p>This feature, previously known as <b>sealed models</b>, has been removed from the library since v4.x.
1422
- It is now available as a custom model
1423
- <a target="_blank" href="docs/examples/sealed.js">available here</a>.
1424
- </p>
1425
- </details>
1426
-
1427
- <details>
1428
- <summary>How should I deal with circular references in my model definitions ?</summary>
1429
- <div class="grid doc-code">
1430
- <div class="doc">
1431
- <p>You can't refer to a model or instance that is not yet defined, so you have to update the
1432
- definition afterwards:</p>
1433
- </div>
1434
- <div class="panel">
1435
- <pre><code class="language-javascript">const Honey = ObjectModel({
1436
- sweetie: undefined // Sweetie is not yet defined
1437
- });
1438
-
1439
- const Sweetie = ObjectModel({
1440
- honey: Honey
1441
- });
1442
-
1443
- Honey.definition.sweetie = [Sweetie];
1444
-
1445
- const joe = Honey({ sweetie: undefined }); // ann is not yet defined
1446
- const ann = Sweetie({ honey: joe });
1447
- joe.sweetie = ann;</code></pre>
1448
- </div>
1449
- </div>
1450
- </details>
1451
-
1452
- <details>
1453
- <summary>How can I serialize/deserialize objects while preserving type information ?</summary>
1454
- <div class="grid doc-code-code">
1455
- <div class="doc">
1456
- <p>Serializing in JSON necessarily implies that you lose the type information, except if you
1457
- store it manually with your data, then retrieve it with a custom parsing function. It is for
1458
- the best to let you decide how you want to store the type information within your data.</p>
1459
- <p>Here is a proposal of implementation using a simple <code>{ _value, _type }</code> wrapper:
1460
- </p>
1461
- </div>
1462
-
1463
- <div class="panel panel1">
1464
- <span class="legend">Implementation</span>
1465
- <pre><code class="language-javascript">Model.prototype.serialize = function(instance, models){
1466
- const names = Object.keys(models);
1467
- return JSON.stringify(instance, function(key, value){
1468
- const modelName = names.find(name => value instanceof models[name]);
1469
- if(modelName && key !== "_value"){
1470
- return { _type: modelName, _value: value }
1471
- }
1472
- return value;
1473
- }, '\t');
1474
- }
1475
-
1476
- Model.prototype.parse = function(json, models){
1477
- return JSON.parse(json, function(key, o){
1478
- if(o && o._type in models){
1479
- return new models[o._type](o._value);
1480
- }
1481
- return o;
1482
- })
1483
- }</code></pre>
1484
- </div>
1485
- <div class="panel panel2">
1486
- <span class="legend">Example</span>
1487
- <pre><code class="language-javascript">const Type1 = ObjectModel({ content: String }).defaultTo({ content: 'Content 1' }),
1488
- Type2 = ObjectModel({ content: String }).defaultTo({ content: 'Content 2' }),
1489
- Container = ObjectModel({ items: ArrayModel([Type1, Type2]) });
1490
-
1491
- // List all your serializable models here
1492
- const serializableModels = { Container, Type1, Type2 };
1493
-
1494
- let a = new Container({ items: [new Type1, new Type2] });
1495
-
1496
- let json = Container.serialize(a, serializableModels);
1497
- console.log(json);
1498
-
1499
- let b = Container.parse(json, serializableModels);
1500
- console.log(
1501
- b instanceof Container,
1502
- b.items[0] instanceof Type1,
1503
- b.items[1] instanceof Type2
1504
- );</code></pre>
1505
- </div>
1506
- </div>
1507
- </details>
1508
-
1509
- <details>
1510
- <summary>Is it possible to convert TypeScript/Flow annotations to Models ?</summary>
1511
- <p>It may be possible with Babel, but ObjectModel is not the best option for this purpose.</p>
1512
- <p>Models have been designed to use all the advantages of runtime validation, such as complex
1513
- assertions, mixed value/type checking or custom error collectors. Compared to static type systems,
1514
- the feature set is really different. I suggest you to combine the strengths of static and dynamic
1515
- type-checking: use static annotations in your business logic layer, and Models at your API
1516
- boundaries to validate user input, network responses or serialized data. You can also use both at
1517
- the same place as demonstrated in the introduction video.</p>
1518
- <p>If you are looking for a runtime type-checking solution that acts as a fully transparent layer over
1519
- an existing static type system, you should try
1520
- <a href="https://codemix.github.io/flow-runtime/" target="_blank" rel="noopener">flow-runtime</a>
1521
- instead.</p>
1522
- </details>
1523
-
1524
- <details>
1525
- <summary>How do I validate values returned by Promises or other async structures ?</summary>
1526
-
1527
- <div class="grid doc-code-code">
1528
- <div class="doc">
1529
- <p>It is impossible to validate both the Promise and the resolved value at the same time,
1530
- because of its asynchronous nature. So it actually requires two different models: a first
1531
- one to check if it is actually a Promise, then a second one to validate the emitted value
1532
- once the promise is resolved. See
1533
- <a href="https://github.com/sylvainpolletvillard/ObjectModel/issues/88" target="_blank"
1534
- rel="noopener">this issue</a>
1535
- for details.</p>
1536
- </div>
1537
-
1538
- <div class="panel panel1">
1539
- <span class="legend">Example helper for Promises</span>
1540
- <pre><code class="language-javascript">const PromiseOf = definition => {
1541
- const PromiseModel = BasicModel(Promise);
1542
- const ResolvedValueModel = Model(definition)
1543
- return p => PromiseModel(p).then(x => ResolvedValueModel(x))
1544
- }</code></pre>
1545
- </div>
1546
- <div class="panel panel2">
1547
- <span class="legend">Usage</span>
1548
- <pre><code class="language-javascript">let p = new Promise(resolve => setTimeout(() => resolve(42), 1000));
1549
-
1550
- PromiseOf(Number)(p).then(n => {
1551
- // ObjectModel has validated both the Promise and the resolved value
1552
- })</code></pre>
1553
- </div>
1554
- </div>
1555
- </details>
1556
-
1557
- </section>
1558
-
1559
- <section>
1560
- <h2>I have a question / suggestion / bug to report</h2>
1561
- <p>Please check the documentation twice, then open an issue on the
1562
- <a href="https://github.com/sylvainpolletvillard/ObjectModel/issues" target="_blank"
1563
- rel="noopener">Github repository</a>
1564
- </p>
1565
- <p>
1566
- You can also ask for support on the
1567
- <a href="https://gitter.im/sylvainpolletvillard/ObjectModel" target="_blank" rel="noopener">
1568
- Gitter channel
1569
- </a>.
1570
- </p>
1571
- </section>
1572
-
1573
- <section>
1574
- <h2>Like what you see ? Share it with the world !</h2>
1575
-
1576
- <ul class="share-buttons">
1577
- <li>
1578
- <a href="https://www.facebook.com/sharer/sharer.php?u=http%3A%2F%2Fobjectmodel.js.org&t=ObjectModel%3A%20Model%20Definition%20and%20Runtime%20Type%20Checking%20for%20JavaScript"
1579
- title="Share on Facebook" target="_blank" rel="noopener"
1580
- onclick="window.open(this.href); return false;">
1581
- <img class="lazy" alt="Facebook" data-src="docs/res/facebook.svg" width="48" height="48">
1582
- </a>
1583
- </li>
1584
- <li>
1585
- <a href="https://twitter.com/intent/tweet?source=http%3A%2F%2Fobjectmodel.js.org&text=ObjectModel%3A%20Model%20Definition%20and%20Runtime%20Type%20Checking%20for%20JavaScript%20-%20http%3A//objectmodel.js.org"
1586
- target="_blank" rel="noopener" title="Tweet" onclick="window.open(this.href); return false;">
1587
- <img class="lazy" alt="Twitter" data-src="docs/res/twitter.svg" width="48" height="48">
1588
- </a>
1589
- </li>
1590
- <li>
1591
- <a href="https://gitter.im/sylvainpolletvillard/ObjectModel?utm_source=website" target="_blank"
1592
- rel="noopener" title="Chat on Gitter">
1593
- <img class="lazy" alt="Gitter" data-src="docs/res/gitter.svg" width="48" height="48">
1594
- </a>
1595
- </li>
1596
- </ul>
1597
-
1598
- </section>
1599
-
1600
- <hr>
1601
- <footer><a href="LICENSE">MIT licensed</a> - Sylvain Pollet-Villard</footer>
1602
- </div>
1603
-
1604
- </body>
1605
-
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="utf-8">
6
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1">
8
+ <meta name="description" content="ObjectModel : Strong Dynamically Typed Object Modeling for JavaScript">
9
+ <meta name="author" content="Sylvain Pollet-Villard">
10
+
11
+ <title>ObjectModel</title>
12
+
13
+ <!-- Global site tag (gtag.js) - Google Analytics -->
14
+ <script async src="https://www.googletagmanager.com/gtag/js?id=UA-74235687-2"></script>
15
+ <script>
16
+ window.dataLayer = window.dataLayer || [];
17
+
18
+ function gtag() {
19
+ dataLayer.push(arguments);
20
+ }
21
+ gtag('js', new Date());
22
+
23
+ gtag('config', 'UA-74235687-2');
24
+ </script>
25
+
26
+ <link rel="icon" type="image/png" href="docs/favicon-32x32.png" sizes="32x32" />
27
+ <link rel="icon" type="image/png" href="docs/favicon-16x16.png" sizes="16x16" />
28
+ <link rel="shortcut icon" href="docs/favicon.ico" type="image/x-icon">
29
+
30
+ <link rel="stylesheet" href="docs/style/main.compiled.css" />
31
+
32
+ <script defer src="docs/js/main.compiled.js"></script>
33
+ <script async type="module">
34
+ import * as globals from "./dist/object-model.js"
35
+ Object.assign(window, globals)
36
+ </script>
37
+ </head>
38
+
39
+ <body>
40
+ <button id="menu-button" class="lines-button arrow arrow-left" aria-label="Toggle Navigation">
41
+ <span class="lines"></span>
42
+ </button>
43
+
44
+ <nav id="menu">
45
+ <a class="title download-link" href="#download">
46
+ <h2>Download</h2>
47
+ </a>
48
+ <hr>
49
+ <a class="title github-link" target="_blank" rel="noopener"
50
+ href="https://github.com/sylvainpolletvillard/ObjectModel">
51
+ <h2>View on Github</h2>
52
+ </a>
53
+ <hr>
54
+ <div>
55
+ <a href="#introduction" class="title">Introduction</a>
56
+ <ul>
57
+ <li><a href="#introduction">What is it</a></li>
58
+ <li><a href="#video-demo">Video demo</a></li>
59
+ <li><a href="#features">Features</a></li>
60
+ </ul>
61
+ </div>
62
+ <hr>
63
+ <div>
64
+ <a href="#doc-basic-model" class="title">Documentation</a>
65
+ <ul>
66
+ <li><a href="#doc-basic-model">Basic models</a></li>
67
+ <li><a href="#doc-object-model">Object models</a></li>
68
+ <li><a href="#doc-model">Model constructor</a></li>
69
+ <li><a href="#doc-es6-classes">Using ES6 class</a></li>
70
+ <li><a href="#doc-optional-properties">Optional properties</a></li>
71
+ <li><a href="#doc-multiple-types">Multiple types</a></li>
72
+ <li><a href="#doc-value-checking">Value checking and enumerations</a></li>
73
+ <li><a href="#doc-null-safe">Null-safe object traversal</a></li>
74
+ <li><a href="#doc-default-values">Default values</a></li>
75
+ <li><a href="#doc-composition">Composition</a></li>
76
+ <li><a href="#doc-extensions">Inheritance by extensions</a></li>
77
+ <li><a href="#doc-multiple-inheritance">Multiple inheritance</a></li>
78
+ <li><a href="#doc-assertions">Assertions for custom tests</a></li>
79
+ <li><a href="#doc-private-and-constants">Private and constant properties</a></li>
80
+ <li><a href="#doc-array-model">Array models</a></li>
81
+ <li><a href="#doc-function-model">Function models</a></li>
82
+ <li><a href="#doc-map-models">Map models</a></li>
83
+ <li><a href="#doc-set-models">Set models</a></li>
84
+ <li><a href="#doc-any-model">Any model</a></li>
85
+ <li><a href="#doc-custom-collectors">Custom error collectors</a></li>
86
+ <li><a href="#doc-custom-devtool-formatters">Custom devtool formatters</a></li>
87
+ <li><a href="#api">Full API</a></li>
88
+ <li><a href="#common-models">Commonly used models</a></li>
89
+ <li><a href="#common-questions">Common questions</a></li>
90
+ </ul>
91
+ </div>
92
+ <hr>
93
+ </nav>
94
+
95
+ <div id="page">
96
+
97
+ <header class="header">
98
+ <img id="logo" src="docs/res/logo.png" alt="ObjectModel" width="800" height="150">
99
+ <hr>
100
+ <p class="description">Strong Dynamically Typed Object Modeling for JavaScript</p>
101
+ </header>
102
+
103
+ <section id="introduction" class="grid">
104
+ <h1 hidden>Introduction</h1>
105
+
106
+ <div class="description">
107
+ <h2 id="what-is-it">What is this library ?</h2>
108
+
109
+ <p>ObjectModel intends to bring <strong>strong dynamic type checking</strong> to your web applications.
110
+ Contrary to static type-checking solutions like <a href="https://www.typescriptlang.org"
111
+ target="_blank" rel="noopener">TypeScript</a> or <a href="https://flowtype.org" target="_blank"
112
+ rel="noopener">Flow</a>, ObjectModel can also validate data at runtime: JSON from the server,
113
+ form inputs, content from local storages, external libraries...</p>
114
+
115
+ <p>By leveraging <strong>ES6 Proxies</strong>, this library ensures that your variables always match the
116
+ model definition and validation constraints you added to them. Thanks to the generated exceptions,
117
+ it will help you spot potential bugs and save you time spent on debugging. ObjectModel is also very
118
+ easy to master: no new language to learn, no new tools, no compilation step, just a minimalist and
119
+ intuitive API in a plain old JS micro-library.</p>
120
+
121
+ <p>Validating at runtime also brings many other benefits: you can define your own types, use them in
122
+ complex model definitions with custom assertions that can even change depending on your application
123
+ state. Actually it goes much further than just type safety. Go on and see for yourself.</p>
124
+ </div>
125
+
126
+ <div id="video-demo">
127
+ <iframe data-src="https://www.youtube.com/embed/zmojfyNH_EE?rel=0&amp;showinfo=0" src="" width="640"
128
+ height="360" frameborder="0" allowfullscreen>
129
+ </iframe>
130
+ </div>
131
+ </section>
132
+
133
+ <hr>
134
+
135
+ <section id="features-and-download" class="grid">
136
+ <div id="features">
137
+ <h2>What's inside the box ?</h2>
138
+ <p>Many features, hopefully neither too much nor too few:</p>
139
+ <ul>
140
+ <li>Typed structures: objects, arrays, maps, sets, functions...</li>
141
+ <li>Union types</li>
142
+ <li>Enumerations</li>
143
+ <li>Custom assertions</li>
144
+ <li>Optional properties</li>
145
+ <li>Default values</li>
146
+ <li>Null-safe object traversal</li>
147
+ <li>Easy composition or inheritance</li>
148
+ <li>Constants and private properties based on name conventions</li>
149
+ <li>Explicit error messages</li>
150
+ <li>Customizable error handlers</li>
151
+ <li>all in <strong class="size-gzip">4.11 kB</strong> minified and gzipped,
152
+ even less when using tree-shaking
153
+ </li>
154
+ </ul>
155
+ </div>
156
+
157
+ <div id="download">
158
+ <h2>Download</h2>
159
+ <h3>Current version: v<span class="version">4.3.1</span></h3>
160
+ <ul>
161
+ <li>
162
+ From <a href="https://www.npmjs.com/package/objectmodel" target="_blank" rel="noopener">
163
+ <abbr title="Node Package Manager">npm</abbr></a>:
164
+ <code>npm install objectmodel</code>
165
+ </li>
166
+ <li>From <abbr title="Content Delivery Network">CDN</abbr>:
167
+ <a href="http://cdn.pika.dev/objectmodel" rel="noopener">cdn.pika.dev/objectmodel</a>
168
+ </li>
169
+ <li>Minified <abbr title="ECMAScript module">ESM</abbr> bundle
170
+ (<strong class="size-gzip">4.11 kB</strong> gzipped) :
171
+ <a href="dist/object-model.min.js">object-model.min.js</a>
172
+ </li>
173
+ <li>Source files :
174
+ <a class="link-zip" href="https://github.com/sylvainpolletvillard/ObjectModel/archive/v4.3.1.zip">object-model-4.3.1.zip</a>
175
+ </li>
176
+ </ul>
177
+
178
+ <h3>Previous versions</h3>
179
+
180
+ <p>If you need to support older browsers, you may have to use an older version of the library instead.
181
+ Please take a look at the <a href="#common-questions"
182
+ onclick="document.getElementById('browser-support').parentNode.open=true">
183
+ Browsers/Node support</a>
184
+ section for more information about browser/Node support for each version.
185
+ </p>
186
+ <p>Full changelog between versions is available on the
187
+ <a href="https://github.com/sylvainpolletvillard/ObjectModel/releases" target="_blank"
188
+ rel="noopener">Github Releases</a>
189
+ page.</p>
190
+
191
+ </div>
192
+ <div>
193
+ <h2>Usage</h2>
194
+ <p>Since v4.0, ObjectModel is shipped in <abbr title="ECMAScript module">ESM</abbr> format, and written
195
+ in modern JavaScript (ES2018).</strong></p>
196
+
197
+ <div class="panel">
198
+ <pre><code class="language-javascript">import { Model } from "objectmodel"</code></pre>
199
+ </div>
200
+
201
+ <p>Not all environments support <abbr title="ECMAScript modules">ESM</abbr> yet, so you should configure
202
+ a transpiler/bundler such as
203
+ Babel/Webpack for your project. If you just want a ready-to-use
204
+ <abbr title="Universal Module Definition">UMD</abbr> version, you can use a transpiling service
205
+ such as <a href="http://pika.dev" target="_blank" rel="noopener">pika.dev</a> or
206
+ <a href="http://unpkg.com" target="_blank" rel="noopener">unpkg.com</a> like this:
207
+ </p>
208
+
209
+ <div class="panel">
210
+ <pre><code class="language-html">&lt;script src="https://umd.cdn.pika.dev/objectmodel/v4"&gt;&lt;/script&gt;</code>
211
+ <code class="language-js">const { Model } = objectmodel</code></pre>
212
+ </div>
213
+ </div>
214
+ </section>
215
+
216
+ <hr>
217
+
218
+ <h1>Documentation</h1>
219
+ <p class="tip">ObjectModel is already loaded on this webpage, so you can try the examples below in your browser
220
+ JavaScript console.</p>
221
+
222
+ <section id="doc-basic-model" class="grid doc-code-code">
223
+ <div class="doc">
224
+ <h2>Basic models</h2>
225
+ <p>Basic models simply validate a variable against the model definition passed as argument, and return
226
+ the validated value. <code>BasicModel</code> constructor takes a <i>model definition</i> as the only
227
+ argument. They are generally used to declare all the basic generic types that you will use in your
228
+ application. You can find a list of <a href="#common-models">common basic models here</a>.</p>
229
+ </div>
230
+
231
+ <div class="panel panel1">
232
+ <span class="legend">Model</span>
233
+ <pre><code class="language-javascript">import { BasicModel } from "objectmodel"
234
+
235
+ const NumberModel = BasicModel(Number);
236
+ // 'new' keyword is optional for models and model instances</code></pre>
237
+ </div>
238
+
239
+ <div class="panel panel2">
240
+ <span class="legend">Instance</span>
241
+ <pre><code class="language-javascript">let x = NumberModel("42");</code>
242
+ <code class="language-none exception">TypeError: expecting Number, got String "42"</code></pre>
243
+ </div>
244
+ </section>
245
+
246
+ <hr>
247
+
248
+ <section id="doc-object-model" class="grid doc-code-code">
249
+ <div class="doc">
250
+ <h2>Object models</h2>
251
+ <p>Object models validate nested object properties against a definition tree. They provide automatic
252
+ validation at initial and future assignments of the properties of the instance objects.</p>
253
+ </div>
254
+
255
+ <div class="panel panel1">
256
+ <span class="legend">Model</span>
257
+ <pre><code class="language-javascript">import { ObjectModel } from "objectmodel"
258
+
259
+ const Order = new ObjectModel({
260
+ product: {
261
+ name: String,
262
+ quantity: Number,
263
+ },
264
+ orderDate: Date
265
+ });</code></pre>
266
+ </div>
267
+
268
+ <div class="panel panel2">
269
+ <span class="legend">Instance</span>
270
+ <pre><code class="language-javascript">const myOrder = new Order({
271
+ product: { name: "Apple Pie", quantity: 1 },
272
+ orderDate: new Date()
273
+ });
274
+
275
+ myOrder.product.quantity = 2; // no exceptions thrown
276
+ myOrder.product.quantity = false; //try to assign a Boolean</code>
277
+ <code class="language-none exception">TypeError: expecting product.quantity to be Number, got Boolean false</code></pre>
278
+ </div>
279
+ </section>
280
+
281
+ <hr>
282
+
283
+ <section id="doc-model" class="grid doc-code">
284
+ <div class="doc">
285
+ <h2>Model constructor</h2>
286
+ <p><code>Model</code> is the <strong>base class of all models</strong> and can be used as an alias for
287
+ <code>BasicModel</code> and <code>ObjectModel</code> constructors.</p>
288
+ </div>
289
+
290
+ <div class="panel">
291
+ <span class="legend">Example</span>
292
+ <pre><code class="language-javascript">import { Model, BasicModel, ObjectModel } from "objectmodel"
293
+
294
+ Model(String) // same as BasicModel(String)
295
+ Model({ name: String }) // same as ObjectModel({ name: String })</code></pre>
296
+ </div>
297
+ </section>
298
+
299
+ <hr>
300
+
301
+ <section id="doc-es6-classes" class="grid doc-code-code">
302
+ <div class="doc">
303
+ <h2>Usage with ES6 classes</h2>
304
+ <p>If you are using ES6 classes in your project, it is very easy to define a model for your classes:</p>
305
+ </div>
306
+
307
+ <div class="panel panel1">
308
+ <span class="legend">Model</span>
309
+ <pre><code class="language-javascript">class Character extends Model({ lastName: String, firstName: String }){
310
+ get fullName(){ return `${this.firstName} ${this.lastName}`; }
311
+ }</code></pre>
312
+ </div>
313
+
314
+ <div class="panel panel2">
315
+ <span class="legend">Instance</span>
316
+ <pre><code class="language-javascript">const rick = new Character({ lastName: "Sanchez", firstName: "Rick" });
317
+ rick.lastName = 132;</code>
318
+ <code class="language-none exception">TypeError: expecting lastName to be String, got Number 132</code>
319
+ <code class="language-javascript">console.log(rick.fullName); // "Rick Sanchez"</code></pre>
320
+ </div>
321
+ </section>
322
+
323
+ <hr>
324
+
325
+ <section id="doc-optional-properties" class="grid doc-code-code">
326
+ <div class="doc">
327
+ <h2>Optional properties</h2>
328
+ <p>By default, model properties are mandatory. That means all properties defined are required on
329
+ instance declaration, otherwise an exception will be raised. But you can specify a property to be
330
+ optional by using the bracket notation, borrowed from the JSDoc specification</p>
331
+ </div>
332
+
333
+ <div class="panel panel1">
334
+ <span class="legend">Model</span>
335
+ <pre><code class="language-javascript">const User = ObjectModel({
336
+ email: String, // mandatory
337
+ name: [String] // optional
338
+ });</code></pre>
339
+ </div>
340
+
341
+ <div class="panel panel2">
342
+ <span class="legend">Instance</span>
343
+ <pre><code class="language-javascript">const stan = User({ email: "stan@smith.com" }); // no exceptions
344
+ const roger = User({ name: "Roger" }); // email is mandatory</code>
345
+ <code class="language-none exception">TypeError: expecting email to be String, got undefined</code></pre>
346
+ </div>
347
+ </section>
348
+
349
+ <hr>
350
+
351
+ <section id="doc-multiple-types" class="grid doc-code-code">
352
+ <div class="doc">
353
+ <h2>Multiple types</h2>
354
+ <p>Several valid types can be specified for one property, aka <strong>union types</strong>.
355
+ So optional properties are actually union types between the original type and the values
356
+ <code>undefined</code> and <code>null</code>. To declare an optional union type, add
357
+ <code>undefined</code> to the list.</p>
358
+ </div>
359
+
360
+ <div class="panel panel1">
361
+ <span class="legend">Model</span>
362
+ <pre><code class="language-javascript">const Animation = new ObjectModel({
363
+ // can be a Number or a String
364
+ delay: [Number, String],
365
+
366
+ // optional property which can be a Boolean or a String
367
+ easing: [Boolean, String, undefined]
368
+ });</code></pre>
369
+ </div>
370
+
371
+ <div class="panel panel2">
372
+ <span class="legend">Instance</span>
373
+ <pre><code class="language-javascript">const opening = new Animation({ delay: 300 }); // easing is optional
374
+ opening.delay = "fast"; // String is a valid type
375
+ opening.delay = null;</code>
376
+ <code class="language-none exception">TypeError: expecting delay to be Number or String, got null</code>
377
+ <code class="language-javascript">opening.easing = true; // Boolean is a valid type
378
+ opening.easing = 1;</code>
379
+ <code class="language-none exception">TypeError: expecting easing to be Boolean or String or undefined, got Number 1</code></pre>
380
+ </div>
381
+ </section>
382
+
383
+ <hr>
384
+
385
+ <section id="doc-value-checking" class="grid doc-code">
386
+ <div class="doc">
387
+ <h2>Value checking and enumerations</h2>
388
+ <p>In model definitions, you can also specify values instead of types for model properties. The property
389
+ value will have to match the model one. Just like union types, use brackets notation for value
390
+ enumerations.
391
+ </p>
392
+ <p>If a regular expression is passed, the value must match it.</p>
393
+ </div>
394
+
395
+ <div class="panel">
396
+ <span class="legend">Model</span>
397
+ <pre><code class="language-javascript">const Shirt = new ObjectModel({
398
+ // the only acceptable value is "clothes"
399
+ category: "clothes",
400
+
401
+ // valid values: 38, 42, "S", "M", "L", "XL", "XXL"...
402
+ size: [Number, "M", /^X{0,2}[SL]$/],
403
+
404
+ // valid values: "black", "#FF0000", undefined...
405
+ color: ["black","white", new RegExp("^#([A-F0-9]{6})$"), undefined]
406
+ });</code></pre>
407
+ </div>
408
+ </section>
409
+
410
+ <hr>
411
+
412
+ <section id="doc-null-safe" class="grid doc-code-code">
413
+ <div class="doc">
414
+ <h2>Null-safe object traversal</h2>
415
+ <p>When you want to traverse nested objects, you always have to worry about the null pointer exception.
416
+ Some languages such as Groovy have a safe navigation operator represented by <code>?.</code> to
417
+ safely navigate through potential null references. In JavaScript, there is no such solution so you
418
+ have to manually check for <code>undefined/null</code> values at each level of the object. But
419
+ within an object model, declared properties are null-safe for traversal:
420
+ every instance complete its structure with undefined properties according to the model definition.
421
+ </p>
422
+ </div>
423
+
424
+ <div class="panel panel1">
425
+ <span class="legend">Model and instanciation</span>
426
+ <pre><code class="language-javascript">const Config = new ObjectModel({
427
+ local: {
428
+ time: {
429
+ format: ["12h","24h", undefined]
430
+ }
431
+ }
432
+ });
433
+
434
+ const config = { local: undefined };
435
+ const new_config = Config(config); // object model</code></pre>
436
+ </div>
437
+
438
+ <div class="panel panel2">
439
+ <span class="legend">Traversal</span>
440
+ <pre><code class="language-javascript">if(config.local.time.format === "12h"){ hour %= 12; }</code>
441
+ <code class="language-none exception">TypeError: Cannot read property 'time' of undefined</code>
442
+
443
+ <code class="language-javascript">// so to prevent this exception, we have to check this way:
444
+ if(config != null
445
+ && config.local != null
446
+ && config.local.time != null
447
+ && config.local.time.format === "12h"){
448
+ hour %= 12;
449
+ }
450
+
451
+ // with object models, no worries :)
452
+ if(new_config.local.time.format === "12h"){ hour %= 12; }
453
+ // new_config.local.time.format returns undefined</code></pre>
454
+ </div>
455
+ </section>
456
+
457
+ <hr>
458
+
459
+ <section id="doc-default-values" class="grid doc-code-code">
460
+ <div class="doc">
461
+ <h2>Default values assignment</h2>
462
+ <p>You can set a default value for any model with <code>model.defaultTo(value)</code>. This default
463
+ value will be used if no argument is passed to the model constructor.</p>
464
+ </div>
465
+
466
+ <div class="panel panel1">
467
+ <span class="legend">Model</span>
468
+ <pre><code class="language-javascript">let N = BasicModel(Number).defaultTo(1)</code></pre>
469
+ </div>
470
+
471
+ <div class="panel panel2">
472
+ <span class="legend">Instance</span>
473
+ <pre><code class="language-javascript">N(5) + N() === 6</code></pre>
474
+ </div>
475
+
476
+ <div class="doc">
477
+ <p>For object models, the <code>defaultTo</code> method can be used to specify default values for some
478
+ properties of your object models. If these are not defined at object instanciation, their default
479
+ value will be assigned. You can also put them in the model prototype if you prefer to rely on
480
+ <a rel="noopener" target="_blank"
481
+ href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain">prototypal
482
+ inheritance</a>.</p>
483
+ </div>
484
+
485
+ <div class="panel panel1">
486
+ <span class="legend">Model</span>
487
+ <pre><code class="language-javascript">const FileInfo = ObjectModel({
488
+ name: String,
489
+ size: [Number],
490
+ creationDate: [Date],
491
+ writable: Boolean
492
+ }).defaultTo({
493
+ name: "Untitled file",
494
+ size: 0,
495
+ writable: true
496
+ });</code></pre>
497
+ </div>
498
+
499
+ <div class="panel panel2">
500
+ <span class="legend">Instance</span>
501
+ <pre><code class="language-javascript">let file = new FileInfo({ writable: false });</code>
502
+ <code class="language-javascript">file.name; // name is mandatory but a default value was passed</code>
503
+ <code class="language-none log">"Untitled file"</code>
504
+ <code class="language-javascript">file.size; // size is optional, but the default value still applies</code>
505
+ <code class="language-none log">0</code>
506
+ <code class="language-javascript">file.creationDate; // no default value was passed for this property</code>
507
+ <code class="language-none log">undefined</code>
508
+ <code class="language-javascript">file.writable; // passed value overrides default value</code>
509
+ <code class="language-none log">false</code>
510
+ <code class="language-javascript">Object.keys(file);</code>
511
+ <code class="language-none log">["name","size","creationDate","writable"]</code></pre>
512
+ </div>
513
+ </section>
514
+
515
+ <hr>
516
+
517
+ <section id="doc-composition" class="grid doc-code-code">
518
+ <div class="doc">
519
+ <h2>Composition with models as types</h2>
520
+ <p>Nested properties definitions can be models too, so you can compose structures of models.</p>
521
+ <p>When a property value matches a model definition, the value is automatically replaced by an instance
522
+ of the corresponding model. This mechanic is referred as <strong>autocasting</strong> and can be
523
+ compared to <a href="https://en.wikipedia.org/wiki/Duck_typing" target="_blank" rel="noopener"
524
+ class="no-hl">
525
+ duck typing
526
+ </a>.
527
+ Autocasting works for object models properties, but also for Array/Map/Set models items when
528
+ inserted, and for FunctionModel arguments and return value.</p>
529
+ <p>This naive approach is very time saving and allows you, for example, to parse composed models from
530
+ JSON in one step. If there is somehow an ambiguity (such as two suitable models within an union
531
+ type), the value is kept unchanged and a warning console message will inform you how to solve this
532
+ ambiguity.</p>
533
+ </div>
534
+
535
+ <div class="panel panel1">
536
+ <span class="legend">Model</span>
537
+ <pre><code class="language-javascript">const Person = ObjectModel({
538
+ name: String,
539
+ age: [Number]
540
+ });
541
+
542
+ const Lovers = ObjectModel({
543
+ husband: Person,
544
+ wife: Person
545
+ });</code></pre>
546
+ </div>
547
+
548
+ <div class="panel panel2">
549
+ <span class="legend">Instance</span>
550
+ <pre><code class="language-javascript">const joe = { name: "Joe", age: 42 };
551
+ const ann = new Person({
552
+ name: joe.name + "'s wife",
553
+ age: joe.age - 5
554
+ });
555
+
556
+ const couple = Lovers({
557
+ husband: joe, // object autocasted
558
+ wife: ann // object model
559
+ });
560
+
561
+ couple.husband instanceof Person === true // has been casted to Person</code></pre>
562
+ </div>
563
+ </section>
564
+
565
+ <hr>
566
+
567
+ <section id="doc-extensions" class="grid doc-code-code">
568
+ <div class="doc">
569
+ <h2>Inheritance by extension</h2>
570
+ <p>Extensions create new models based on existing model definitions. You can declare new properties or
571
+ override previous ones. Therefore, it is an easy way to reproduce subtyping and class inheritance
572
+ patterns.</p>
573
+ </div>
574
+
575
+ <div class="panel panel1">
576
+ <span class="legend">Model</span>
577
+ <pre><code class="language-javascript">const Person = ObjectModel({
578
+ name: String,
579
+ female: Boolean
580
+ });
581
+
582
+ const Mother = Person.extend({
583
+ female: true,
584
+ child: Person
585
+ });</code></pre>
586
+ </div>
587
+
588
+ <div class="panel panel2">
589
+ <span class="legend">Instance</span>
590
+ <pre><code class="language-javascript">let joe = new Person({ name: "Joe", female: false });
591
+ let ann = new Person({ name: "Ann", female: true });
592
+ let joanna = new Person({ name: "Joanna", female: true });
593
+
594
+ ann = new Mother({ name: "Ann", female: true, child: joanna })
595
+ ann instanceof Mother && ann instanceof Person // true</code>
596
+
597
+ <code class="language-javascript">joe = Mother(joe); // try to cast joe to Mother model</code>
598
+ <code class="language-none exception">TypeError: expecting female to be true, got Boolean false
599
+ expecting child to be {
600
+ name: String,
601
+ female: Boolean
602
+ }, got undefined</code></pre>
603
+ </div>
604
+
605
+ <div class="doc">
606
+ <h3>With ES6 classes</h3>
607
+ <p>Extended models inherit the parent's prototype chain, so you can easily combine it with class
608
+ inheritance. Just make sure to respect the <a
609
+ href="https://en.wikipedia.org/wiki/Liskov_substitution_principle" target="_blank"
610
+ rel="noopener">Liskov substitution principle</a>
611
+ when you extend a type definition.</p>
612
+ </div>
613
+
614
+ <div class="panel panel1">
615
+ <span class="legend">Model</span>
616
+ <pre><code class="language-javascript">class Person extends ObjectModel({ name: String, female: Boolean }){
617
+ constructor({ name, female }){
618
+ if(!female) name = `Mr ${name}`
619
+ super({ name, female })
620
+ }
621
+ }
622
+
623
+ class Mother extends Person.extend({ female: true, child: Person }){
624
+ constructor({ name, female, child }){
625
+ super({ name: `Mrs ${name}`, female, child })
626
+ }
627
+ }</code></pre>
628
+ </div>
629
+
630
+ <div class="panel panel2">
631
+ <span class="legend">Instance</span>
632
+ <pre><code class="language-javascript">let joe = new Person({ name: "Joe", female: false })
633
+ let joanna = new Person({ name: "Joanna", female: true })
634
+ let ann = new Mother({ name: "Ann", female: true, child: joanna })</code>
635
+
636
+ <code class="language-javascript">joe.name</code>
637
+ <code class="language-none log">Mr Joe</code>
638
+ <code class="language-javascript">ann.name</code>
639
+ <code class="language-none log">Mrs Ann</code>
640
+ <code class="language-javascript">ann.child.name</code>
641
+ <code class="language-none log">Joanna</code></pre>
642
+ </div>
643
+
644
+ </section>
645
+
646
+ <hr>
647
+
648
+ <section id="doc-multiple-inheritance" class="grid doc-code-code">
649
+ <div class="doc">
650
+ <h2>Multiple inheritance</h2>
651
+ <p>But it goes further: you can do multiple inheritance and mix any number of parent models definitions
652
+ and assertions. If some properties have the same name, those of the last object overrides the
653
+ others.</p>
654
+ </div>
655
+
656
+ <div class="panel panel1">
657
+ <span class="legend">Model</span>
658
+ <pre><code class="language-javascript">const Client = Person.extend(User, Order, { store: String });
659
+
660
+ Client.prototype.sendConfirmationMail = function(){
661
+ return this.email + ": Dear " + this.name
662
+ + ", thank you for ordering "
663
+ + this.product.quantity + " " + this.product.name
664
+ + " on " + this.store;
665
+ };
666
+
667
+ Object.keys(Client.definition);</code>
668
+ <code class="language-none log">["name", "female", "email", "product", "orderDate", "store"]</code></pre>
669
+ </div>
670
+
671
+ <div class="panel panel2">
672
+ <span class="legend">Instance</span>
673
+ <pre><code class="language-javascript">const joe = new Client({
674
+ name: "Joe",
675
+ female: false,
676
+ email: "joe@email.net",
677
+ product: { name: "diapers", quantity: 100 },
678
+ orderDate: new Date(),
679
+ store: "daddy.net"
680
+ });
681
+
682
+ joe.sendConfirmationMail();</code>
683
+ <code class="language-none log">joe@email.net: Dear Joe, thank you for ordering 100 diapers on daddy.net</code></pre>
684
+ </div>
685
+ </section>
686
+
687
+ <hr>
688
+
689
+ <section id="doc-assertions" class="grid doc-code-code">
690
+ <div class="doc">
691
+ <h2>Assertions for custom validation tests</h2>
692
+ <p>You can add to your models any number of assertions that are custom test functions applied on model
693
+ instances. All assertions are called every time the model is changed, and must all return
694
+ <code>true</code> to validate. Exceptions thrown in assertions are catched and considered as
695
+ assertion failures.</p>
696
+ <p>For example, we can get an Integer model by adding <code>Number.isInteger</code> as an assertion to a
697
+ basic <code>Number</code> model.</p>
698
+ <p>Assertions are inherited from the model prototype, so you can add global assertions on all models by
699
+ setting them in <code>Model.prototype</code>. The second argument of the <code>assert</code> method
700
+ is an optional message shown when assertion fails. It can be a String or a function returning a
701
+ String.</p>
702
+ </div>
703
+
704
+ <div class="panel panel1">
705
+ <span class="legend">Model</span>
706
+ <pre><code class="language-javascript">const PositiveInteger = BasicModel(Number)
707
+ .assert(Number.isInteger)
708
+ .assert(n => n >= 0, "should be greater or equal to zero")
709
+
710
+ function isPrime(n) {
711
+ for (let i=2, m=Math.sqrt(n); i <= m ; i++){
712
+ if(n%i === 0) return false;
713
+ }
714
+ return n > 1;
715
+ }
716
+
717
+ const PrimeNumber = PositiveInteger.extend().assert(isPrime);
718
+ // extend to not add isPrime assertion to the Integer model
719
+ </code></pre>
720
+ </div>
721
+
722
+ <div class="panel panel2">
723
+ <span class="legend">Instance</span>
724
+ <pre><code class="language-javascript">PositiveInteger(-1);</code>
725
+ <code class="language-none exception">TypeError: assertion should be greater or equal to zero returned false for value -1</code>
726
+
727
+ <code class="language-javascript">PositiveInteger(Math.sqrt(2));</code>
728
+ <code class="language-none exception">TypeError: assertion isInteger returned false for value 1.414213562373</code>
729
+
730
+ <code class="language-javascript">PrimeNumber(83);</code>
731
+ <code class="language-none log">83</code>
732
+
733
+ <code class="language-javascript">PrimeNumber(87);</code>
734
+ <code class="language-none exception">TypeError: assertion isPrime returned false for value 87</code></pre>
735
+ </div>
736
+ </section>
737
+
738
+ <hr>
739
+
740
+ <section id="doc-private-and-constants" class="grid doc-code-code">
741
+ <div class="doc">
742
+ <h2>Private and constant properties</h2>
743
+ <p>Some variable naming conventions are commonly used in JavaScript. For example, a leading underscore
744
+ is used to specify a <i>_private</i> property which should not be used outside the object own
745
+ methods. Also, constants are often in <i>ALL_CAPS</i>. Model definitions follow these conventions by
746
+ making <i>_underscored</i> properties not enumerable and not usable outside of the instance's own
747
+ methods, and <i>CAPITALIZED</i> properties not writable.</p>
748
+ <p>Note: private properties access is granted only when using the instance own methods. Methods declared
749
+ in an extended class cannot access to privates. Asynchronous callbacks do not work neither, except
750
+ if these callbacks are defined as methods of the model. If this does not fit your usecase, you
751
+ should probably not make these properties private.</p>
752
+ </div>
753
+
754
+ <div class="panel panel1">
755
+ <span class="legend">Model</span>
756
+ <pre><code class="language-javascript">const Circle = ObjectModel({
757
+ radius: Number, // public
758
+ _index: Number, // private
759
+ UNIT: ["px","cm"], // constant
760
+ _ID: [Number], // private and constant
761
+ }).defaultTo({
762
+ _index: 0,
763
+ getIndex(){ return this._index },
764
+ setIndex(value){ this._index = value }
765
+ });
766
+ </code></pre>
767
+ </div>
768
+
769
+ <div class="panel panel2">
770
+ <span class="legend">Instance</span>
771
+ <pre><code class="language-javascript">let c = new Circle({ radius: 120, UNIT: "px", _ID: 1 });
772
+ c.radius = 100;
773
+ c.UNIT = "cm";</code>
774
+ <code class="language-none exception">TypeError: cannot modify constant property UNIT</code>
775
+
776
+ <code class="language-javascript">c._index = 1;</code>
777
+ <code class="language-none exception">TypeError: cannot modify private property _index</code>
778
+ <code class="language-javascript">console.log( c._index )</code>
779
+ <code class="language-none exception">TypeError: cannot access to private property _index</code>
780
+ <code class="language-javascript">c.setIndex(2);
781
+ console.log( c.getIndex() )</code>
782
+ <code class="language-none log">2</code>
783
+ <code class="language-javascript">Object.keys(c); // private variables are not enumerated</code>
784
+ <code class="language-none log">["radius", "UNIT"]</code></pre>
785
+ </div>
786
+
787
+ <div class="doc">
788
+ <p>You can modify or remove these conventions by overriding the
789
+ <code>conventionForPrivate</code> and
790
+ <code>conventionForConstant</code> methods in your model or globally in
791
+ <code>Model.prototype</code>.
792
+ </p>
793
+ </div>
794
+
795
+ <div class="panel panel1">
796
+ <pre><code class="language-javascript">// change the private convention for all models
797
+ Model.prototype.conventionForPrivate = key => key.startsWith('#');
798
+
799
+ // remove the constant convention specifically for Circle
800
+ Circle.conventionForConstant = () => false;</code></pre>
801
+ </div>
802
+
803
+ <div class="panel panel2">
804
+ <pre><code class="language-javascript">// Private and constant conventions have been changed
805
+ c._index = 3;
806
+ c.UNIT = "cm";
807
+
808
+ console.log(c._index, c.UNIT); // no more errors</code>
809
+ <code class="language-none log">3 "cm"</code></pre>
810
+ </div>
811
+ </section>
812
+ <hr>
813
+
814
+ <section id="doc-array-model" class="grid doc-code-code">
815
+ <div class="doc">
816
+ <h2>Array models</h2>
817
+ <p>Array models validate the type of all elements in an array.</p>
818
+ <p>The validation is done on initial array elements passed to the model, then on new elements added or
819
+ modified afterwards.</p>
820
+ </div>
821
+
822
+ <div class="panel panel1">
823
+ <span class="legend">Model</span>
824
+ <pre><code class="language-javascript">import { ArrayModel } from "objectmodel";
825
+
826
+ const Cards = new ArrayModel([Number, "J","Q","K"]);
827
+
828
+ // Hand is an array of 2 Numbers, J, Q, or K
829
+ const Hand = Cards.extend()
830
+ .assert(a => a.length === 2, "should have two cards");</code></pre>
831
+ </div>
832
+
833
+ <div class="panel panel2">
834
+ <span class="legend">Instance</span>
835
+ <pre><code class="language-javascript">const myHand = Hand( [7, "K"] );
836
+ myHand[0] = "Joker"</code>
837
+ <code class="language-none exception">TypeError: expecting Array[0] to be Number or "J" or "Q" or "K", got String "Joker"</code>
838
+ <code class="language-javascript">myHand.push("K");</code>
839
+ <code class="language-none exception">TypeError: assertion "should have two cards" returned false for value [7, "Joker", "K"]</code></pre>
840
+ </div>
841
+
842
+ <div class="doc">
843
+ <p>All the validation options for previous models are also available for array model elements:
844
+ type/value checking, optional properties, union types, enumerations, assertions...</p>
845
+ </div>
846
+
847
+ <div class="panel panel1">
848
+ <span class="legend">Model</span>
849
+ <pre><code class="language-javascript">const Family = ObjectModel({
850
+ father: Father,
851
+ mother: Mother,
852
+ children: ArrayModel(Person), // array of Persons
853
+ grandparents: [ArrayModel([Mother, Father])]
854
+ // optional array of Mothers or Fathers
855
+ });</code></pre>
856
+ </div>
857
+
858
+ <div class="panel panel2">
859
+ <span class="legend">Instance</span>
860
+ <pre><code class="language-javascript">const joefamily = new Family({
861
+ father: joe,
862
+ mother: ann,
863
+ children: [joanna, "dog"]
864
+ });</code>
865
+ <code class="language-none exception">TypeError: expecting Array[1] to be { name: String, female: Boolean }, got String "dog"</code>
866
+ </pre>
867
+ </div>
868
+ </section>
869
+
870
+ <hr>
871
+
872
+ <section id="doc-function-model" class="grid doc-code-code">
873
+ <div class="doc">
874
+ <h2>Function models</h2>
875
+ <p>Function models provide validation on input (arguments) and output (return value). All the validation
876
+ options for Object models are also available for Function models. The arguments passed to
877
+ <code>FunctionModel</code> are the types of the arguments the function will receive, and the
878
+ <code>return</code> method is used to specify the type of the function return value.</p>
879
+ </div>
880
+
881
+ <div class="panel panel1">
882
+ <pre><code class="language-javascript">import { FunctionModel, BasicModel } from "objectmodel";
883
+
884
+ const Numb = BasicModel(Number).assert(Number.isFinite);
885
+ const Operator = BasicModel(["+","-","*","/"])
886
+
887
+ const Calculator = FunctionModel(Numb, Operator, Numb).return(Numb);
888
+
889
+ const calc = new Calculator((a, operator, b) => eval(a + operator + b));
890
+ </code></pre>
891
+ </div>
892
+
893
+ <div class="panel panel2">
894
+ <pre><code class="language-javascript">calc(3, "+", 1);</code>
895
+ <code class="language-none log">4</code>
896
+ <code class="language-javascript">calc(6, "*", null);</code>
897
+ <code class="language-none exception">TypeError: expecting arguments[2] to be Number, got null</code>
898
+ <code class="language-javascript">calc(1, "/", 0);</code>
899
+ <code class="language-none exception">TypeError: assertion "isFinite" returned false for value Infinity</code></pre>
900
+ </div>
901
+
902
+ <div class="doc">
903
+ <p>In classical JavaScript OOP programming, methods are declared in the constructor's
904
+ <code>prototype</code>. You can do the same with instances of function models.</p>
905
+ <p>Another option is to provide a default implementation in the model definition by using the
906
+ <code>defaultTo</code> method. See the <a href="#doc-default-values">Default values</a> section.
907
+ The difference is that all the properties in the model definition are required for an object
908
+ to be considered suitable for the model. In the following example, an object must have a function
909
+ <code>sayMyName</code> to be valid as a Person, while the function <code>greet</code> is not
910
+ mandatory.</p>
911
+ </div>
912
+
913
+ <div class="panel panel1">
914
+ <span class="legend">Model</span>
915
+ <pre><code class="language-javascript">const Person = ObjectModel({
916
+ name: String,
917
+ // function without arguments returning a String
918
+ sayMyName: FunctionModel().return(String)
919
+ }).defaultTo({
920
+ sayMyName: function(){ return "my name is " + this.name }
921
+ })
922
+
923
+ // takes one Person as argument, returns a String
924
+ Person.prototype.greet = FunctionModel(Person).return(String)(
925
+ function(otherguy){
926
+ return "Hello "+ otherguy.name + ", " + this.sayMyName()
927
+ }
928
+ )</code></pre>
929
+ </div>
930
+
931
+ <div class="panel panel2">
932
+ <span class="legend">Instance</span>
933
+ <pre><code class="language-javascript">const joe = new Person({ name: "Joe" });
934
+
935
+ joe.sayMyName();</code>
936
+ <code class="language-none log">my name is Joe</code>
937
+ <code class="language-javascript">joe.greet({ name: "Ann", greet: "hi ?" });</code>
938
+ <code class="language-none log">Hello Ann, my name is Joe</code>
939
+ <code class="language-javascript">joe.greet({ name: "dog", sayMyName: "woof !" });</code>
940
+ <code class="language-none exception">TypeError: expecting arguments[0].sayMyName to be "Function", got String "woof !"</code></pre>
941
+ </div>
942
+
943
+ </section>
944
+
945
+ <hr>
946
+
947
+ <section id="doc-map-models" class="grid doc-code-code">
948
+ <div class="doc">
949
+ <h2>Map models</h2>
950
+ <p>Map models validate ES6 <code>Map</code> objects by checking both keys and values. The arguments
951
+ passed to <code>MapModel</code> are respectively the definition for the keys and the definition for
952
+ the values.</p>
953
+ </div>
954
+
955
+ <div class="panel panel1">
956
+ <span class="legend">Model</span>
957
+ <pre><code class="language-javascript">import { MapModel, Model } from "objectmodel";
958
+
959
+ const Course = Model([ "math", "english", "history" ])
960
+ const Grade = Model([ "A", "B", "C" ])
961
+
962
+ const Gradebook = MapModel(Course, Grade)
963
+ </code></pre>
964
+ </div>
965
+
966
+ <div class="panel panel2">
967
+ <span class="legend">Instance</span>
968
+ <pre><code class="language-javascript">const joannaGrades = new Gradebook([
969
+ ["math", "B"],
970
+ ["english", "C"]
971
+ ])
972
+
973
+ joannaGrades.set("videogames", "A")</code>
974
+ <code class="language-none exception">TypeError: expecting Map key to be "math" or "english" or "history", got String "videogames"</code>
975
+ <code class="language-javascript">joannaGrades.set("history", "nope")</code>
976
+ <code class="language-none exception">TypeError: expecting Map["history"] to be "A" or "B" or "C" , got String "nope"</code></pre>
977
+ </div>
978
+ </section>
979
+
980
+ <hr>
981
+
982
+ <section id="doc-set-models" class="grid doc-code-code">
983
+ <div class="doc">
984
+ <h2>Set models</h2>
985
+ <p>Set models validate ES6 <code>Set</code> objects by checking the type of all the elements in the set.
986
+ The API is the same as array models.</p>
987
+ </div>
988
+
989
+ <div class="panel panel1">
990
+ <span class="legend">Model</span>
991
+ <pre><code class="language-javascript">import { SetModel, Model } from "objectmodel";
992
+
993
+ const Course = Model([ "math", "english", "history" ])
994
+
995
+ const FavoriteCourses = SetModel(Course)
996
+ </code></pre>
997
+ </div>
998
+
999
+ <div class="panel panel2">
1000
+ <span class="legend">Instance</span>
1001
+ <pre><code class="language-javascript">const joannaFavorites = FavoriteCourses([ "math", "english" ])
1002
+
1003
+ joannaFavorites.add("sleeping")</code>
1004
+ <code class="language-none exception">TypeError: expecting Set value to be "math" or "english" or "history", got String "sleeping"</code></pre>
1005
+ </div>
1006
+ </section>
1007
+
1008
+ <hr>
1009
+
1010
+ <section id="doc-any-model" class="grid doc-code">
1011
+ <div class="doc">
1012
+ <h2>Any model</h2>
1013
+ <p>The <code>Any</code> model is used to define a property or parameter that can take <i>any</i> value.
1014
+ It is better than an union type with all primitives and objects, as it skips every validation step
1015
+ instead of checking every possible type.</p>
1016
+ </div>
1017
+
1018
+ <div class="panel">
1019
+ <pre><code class="language-javascript">import { Any, ObjectModel, ArrayModel, FunctionModel } from "objectmodel";
1020
+
1021
+ // examples using the Any Model
1022
+ const DataWrapper = ObjectModel({ data: Any })
1023
+ const ArrayNotEmpty = ArrayModel(Any).assert(arr => arr.length > 0)
1024
+ const Serializer = FunctionModel(Any).return(String);</code></pre>
1025
+ </div>
1026
+ </section>
1027
+
1028
+ <section id="doc-any-remainining" class="grid doc-code">
1029
+ <div class="doc">
1030
+ <h2>...Any remaining parameter</h2>
1031
+ <p>The <code>Any</code> model can also be used as <code>...Any</code> in <code>FunctionModel</code>
1032
+ parameters definition to specify a function that can take <i>any</i> amount of parameters.
1033
+ <code>...Any</code> is a straight-forward syntax for functions already using the ES6
1034
+ <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters"
1035
+ target="_blank" rel="noopener">rest operator</a>.</p>
1036
+ <p>The remaining parameters can be checked against a definition passed as argument
1037
+ (<code>...Any(def)</code>. That definition defaults to
1038
+ <code>Any</code> if not specified.</p>
1039
+ </div>
1040
+
1041
+ <div class="panel">
1042
+ <pre><code class="language-javascript">import { Any, FunctionModel } from "objectmodel";
1043
+
1044
+ // takes 2 parameters or more
1045
+ const Operation = FunctionModel(Number, Number, ...Any)
1046
+
1047
+ // takes any amount of Numbers as parameters
1048
+ const NumericOperation = FunctionModel(...Any(Number)).return(Number)</code></pre>
1049
+ </div>
1050
+ </section>
1051
+
1052
+ <hr>
1053
+
1054
+ <section id="doc-custom-collectors" class="grid doc-code-code">
1055
+ <h2>Custom error collectors</h2>
1056
+ <div class="doc">
1057
+ <p>By default, validation errors are collected every time a model instance is created or modified, and
1058
+ thrown as <code>TypeError</code> exceptions with a message describing all the errors found. It it
1059
+ possible to change this behaviour and add your own error collectors. For example, you may want to
1060
+ notify the user that an error occurred, or send the information to your server for error tracking on
1061
+ production.</p>
1062
+ </div>
1063
+ <div class="doc">
1064
+ <p>Error collectors are callback functions called with an array of all the errors collected during the
1065
+ last model inspection. Every error is an object with these properties:</p>
1066
+ <ul>
1067
+ <li><code>message</code>: a message describing the error</li>
1068
+ <li><code>expected</code>: the expected type definition or assertion/li>
1069
+ <li><code>received</code>: the received value, to compare to the expected</li>
1070
+ <li><code>path</code>: the path where the error occurred in an object model definition</li>
1071
+ </ul>
1072
+ </div>
1073
+
1074
+ <div class="doc">
1075
+ <h3>Global error collector</h3>
1076
+ <p>This is how you define an error collector globally for all models.</p>
1077
+ </div>
1078
+
1079
+ <div class="panel panel1">
1080
+ <pre><code class="language-javascript">Model.prototype.errorCollector = function(errors){
1081
+ console.log("Global error collector caught these errors:");
1082
+ errors.forEach(error => { console.dir(error) });
1083
+ };
1084
+
1085
+ const Student = ObjectModel({
1086
+ name: String,
1087
+ course: [ "math","english","history" ],
1088
+ grade: Number
1089
+ }).assert(student => student.grade >= 60,
1090
+ "should at least get 60 to validate semester")
1091
+
1092
+ new Student({ name: "Joanna", course: "sleep", grade: 0 });</code></pre>
1093
+ </div>
1094
+
1095
+ <div class="panel panel2">
1096
+ <span class="legend">Result</span>
1097
+ <pre><code class="language-none log">Global error collector caught these errors:
1098
+ {
1099
+ message: 'expecting course to be "math" or "english" or "history", got String "sleep"'
1100
+ path: "course"
1101
+ expected: ["math","english","history"]
1102
+ received: "sleep"
1103
+ }
1104
+ {
1105
+ message: "assertion should at least get 60 to validate semester returned false for value { name: "Joanna", course: "sleep", grade: 0 }",
1106
+ path: null,
1107
+ expected: student => student.grade >= 60,
1108
+ received: { name: "Joanna", course: "sleep", grade: 0 }
1109
+ }</code></pre>
1110
+ </div>
1111
+
1112
+ <div class="doc">
1113
+ <h3>Model error collector</h3>
1114
+ <p>This is how you define an error collector specifically by model</p>
1115
+ </div>
1116
+
1117
+ <div class="panel panel1">
1118
+ <pre><code class="language-javascript">Student.errorCollector = function(errors){
1119
+ console.log("Student model error collector caught these errors:");
1120
+ errors.forEach(error => { console.dir(error) });
1121
+ };
1122
+
1123
+ new Student({ name: "Joanna", course: "math", grade: 50 });</code></pre>
1124
+ </div>
1125
+
1126
+ <div class="panel panel2">
1127
+ <span class="legend">Result</span>
1128
+ <pre><code class="language-none log">Student model collector caught these errors:
1129
+ {
1130
+ message: "assertion should at least get 60 to validate semester returned false for value { name: "Joanna", course: "math", grade: 50 }",
1131
+ path: null,
1132
+ expected: student => student.grade >= 60,
1133
+ received: { name: "Joanna", course: "math", grade: 50 }
1134
+ }</code></pre>
1135
+ </div>
1136
+
1137
+ <div class="doc">
1138
+ <h3>Single-use error collector</h3>
1139
+ <p>And this is how you define an error collector to be used only once with
1140
+ <code>test(obj, myErrorCollector)</code></p>
1141
+ </div>
1142
+
1143
+ <div class="panel panel1">
1144
+ <pre><code class="language-javascript">Student.test({
1145
+ name: "Joanna",
1146
+ course: "cheating",
1147
+ grade: 90
1148
+ }, function(errors){
1149
+ console.log("This specific error collector caught these errors:");
1150
+ errors.forEach(error => { console.dir(error) });
1151
+ });</code></pre>
1152
+ </div>
1153
+
1154
+ <div class="panel panel2">
1155
+ <span class="legend">Result</span>
1156
+ <pre><code class="language-none log">This specific error collector caught these errors:
1157
+ {
1158
+ message: 'expecting course to be "math" or "english" or "history", got String "cheating"'
1159
+ path: "course"
1160
+ expected: ["math","english","history","science"]
1161
+ received: "cheating"
1162
+ }</code></pre>
1163
+ </div>
1164
+
1165
+ </section>
1166
+
1167
+ <hr>
1168
+
1169
+ <section id="doc-custom-devtool-formatters">
1170
+ <h2>Custom devtool formatters</h2>
1171
+
1172
+ <p>ObjectModel provides custom formatters for Models and Model instances in Chrome Developer Tools,
1173
+ available in Chrome and Opera. These formatters improve the way models and instances are displayed when
1174
+ logged in the console.</p>
1175
+
1176
+ <h3>Enabling custom formatters</h3>
1177
+
1178
+ <p>Chrome currently doesn’t have custom formatters enabled by default. You need to enter the DevTools
1179
+ settings via the menu at the top right of the DevTools panel, then select <i>Preferences</i> and check
1180
+ <i>Enable custom formatters</i> in the <i>Console</i> section.</p>
1181
+
1182
+ <div>
1183
+ <figure style="display: inline-block; vertical-align: top">
1184
+ <img class="lazy" alt="Enabling custom formatters in Chrome"
1185
+ data-src="docs/res/custom-formatters-chrome-settings.jpg">
1186
+ </figure>
1187
+
1188
+ <figure style="display: inline-block; vertical-align: top">
1189
+ <img class="lazy" alt="Screenshot without custom formatters"
1190
+ data-src="docs/res/custom-formatters-before.jpg">
1191
+ <img class="lazy" alt="Screenshot with custom formatters"
1192
+ data-src="docs/res/custom-formatters-after.jpg">
1193
+ </figure>
1194
+ </div>
1195
+
1196
+ <p>The formatters for ObjectModel are <strong>included by default in the unminified bundle
1197
+ (<code>dist/object-model.js</code>)</strong>.
1198
+ If you are using modules, import them manually with :</p>
1199
+ <pre><code class="language-javascript">import * from "objectmodel/src/devtool-formatter"</code></pre>
1200
+
1201
+ <h3>Specifying the Model Name <small>(since v3.4)</small></h3>
1202
+ <p>A variable name is irrelevant to name a Model, because several variables with different names can point
1203
+ to the same Model reference. To specify a unique model name for debugging purposes, you can use the
1204
+ <code>as()</code> method like this :</p>
1205
+ <pre><code class="language-javascript">const Integer = Model(Number).assert(Number.isInteger).as("Integer");
1206
+ console.log(Integer.name) // "Integer"</code></pre>
1207
+ </section>
1208
+
1209
+ <hr>
1210
+
1211
+ <section id="api">
1212
+ <h2>Full API</h2>
1213
+ <h3>Imported from objectmodel root scope</h3>
1214
+ <dl>
1215
+ <dt><a href="#doc-model">Model</a> <code>Model(definition)</code></dt>
1216
+ <dd>Constructor alias for basic and object models</dd>
1217
+
1218
+ <dt><a href="#doc-basic-model">BasicModel</a> <code>BasicModel(definition)</code></dt>
1219
+ <dd>Constructor for basic models</dd>
1220
+
1221
+ <dt><a href="#doc-object-model">ObjectModel</a> <code>ObjectModel(definition)</code></dt>
1222
+ <dd>Constructor for object models</dd>
1223
+
1224
+ <dt><a href="#doc-array-model">ArrayModel</a> <code>ArrayModel(itemDefinition)</code></dt>
1225
+ <dd>Constructor for array models</dd>
1226
+
1227
+ <dt><a href="#doc-function-model">FunctionModel</a> <code>FunctionModel(definitionArgument1,
1228
+ definitionArgument2, ...)</code></dt>
1229
+ <dd>Constructor for function models</dd>
1230
+
1231
+ <dt><a href="#doc-map-model">MapModel</a> <code>MapModel(keyDefinition, valueDefinition)</code></dt>
1232
+ <dd>Constructor for map models</dd>
1233
+
1234
+ <dt><a href="#doc-set-model">SetModel</a> <code>SetModel(itemDefinition)</code></dt>
1235
+ <dd>Constructor for set models</dd>
1236
+
1237
+ </dl>
1238
+ <h3>Model methods and properties</h3>
1239
+ <dl>
1240
+ <dt>name <code>model.name</code></dt>
1241
+ <dd>The name of the model, used for debugging purposes</dd>
1242
+
1243
+ <dt>as <code>model.as(newName)</code></dt>
1244
+ <dd>Set the name of the model</dd>
1245
+
1246
+ <dt>definition <code>model.definition</code></dt>
1247
+ <dd>Returns the model definition</dd>
1248
+
1249
+ <dt><a href="#doc-extensions">extend</a> <code>model.extend(...otherModelsOrDefinitions)</code></dt>
1250
+ <dd>Returns a new model based on the initial model merged with other definitions/assertions</dd>
1251
+
1252
+ <dt>assertions <code>model.assertions</code></dt>
1253
+ <dd>Returns the list of model assertions</dd>
1254
+
1255
+ <dt><a href="#doc-assertions">assert</a> <code>model.assert(assertion, [description])</code></dt>
1256
+ <dd>Add a test function to the model that must return <code>true</code> to validate the instance.</dd>
1257
+
1258
+ <dt>default <code>model.default</code></dt>
1259
+ <dd>Returns the default value if defined</dd>
1260
+
1261
+ <dt><a href="#doc-default-values">defaultTo</a> <code>model.defaultTo(defaultValue)</code></dt>
1262
+ <dd>Set the default value of the model</dd>
1263
+
1264
+ <dt>errorCollector <code>model.errorCollector = function(errors){ ... }</code></dt>
1265
+ <dd>Function called when validation errors are detected</dd>
1266
+
1267
+ <dt><a href="#doc-composition">test</a> <code>model.test(value, [errorCollector])</code></dt>
1268
+ <dd>Returns <code>true</code> if the value passed validates the model definition.
1269
+ Works with <a href="#doc-composition">autocasting</a>.
1270
+ A custom error collector can be specified to retrieve the validation errors.</dd>
1271
+
1272
+ <dt><a href="#doc-private-and-constants">conventionForConstant</a> <code>function(variableName)</code>
1273
+ </dt>
1274
+ <dd>Internal function used to identify a constant property based on naming convention. You can override
1275
+ it to suit your needs.</dd>
1276
+
1277
+ <dt><a href="#doc-private-and-constants">conventionForPrivate</a> <code>function(variableName)</code>
1278
+ </dt>
1279
+ <dd>Internal function used to identify a non-enumerable property based on naming convention. You can
1280
+ override it to suit your needs.</dd>
1281
+
1282
+ </dl>
1283
+ <h3>Object models</h3>
1284
+ <dl>
1285
+ <dt>defaultTo <code>objectModel.defaultTo(defaultValuesObject)</code></dt>
1286
+ <dd>Set the default values for some model properties.</dd>
1287
+ </dl>
1288
+ <h3>Function models</h3>
1289
+ <dl>
1290
+ <dt>return <code>functionModel.return(returnValueDefinition)</code></dt>
1291
+ <dd>Set the definition of the return value. Each call to the function must return a validated value,
1292
+ otherwise an exception will be raised.</dd>
1293
+ </dl>
1294
+ </section>
1295
+
1296
+ <hr>
1297
+
1298
+ <section id="common-models">
1299
+ <h2>Commonly used models</h2>
1300
+ <p>Here are some models that you may find useful. <strong>These are not included in the library</strong>, so
1301
+ pick what you need or <a href="docs/examples/common.js">get them all from here</a></p>
1302
+
1303
+ <pre><code class="language-javascript" data-source="docs/examples/common.js"></code></pre>
1304
+ </section>
1305
+ <hr>
1306
+
1307
+ <section id="common-questions">
1308
+ <h2>Common questions</h2>
1309
+
1310
+ <details>
1311
+ <summary id="browser-support">Which browsers and node versions are supported ?</summary>
1312
+ <p>This library is <a href="test/" target="_blank">unit tested</a> against these browsers and Node.js
1313
+ versions, depending of the version of Object Model:</p>
1314
+ <dl>
1315
+ <dt><a href="docs/v1/">v1.x</a></dt>
1316
+ <dd>Chrome 29+, Firefox 24+, Edge, Internet Explorer 9+, Opera 20+, Safari 5.1+, Node.js 4.0+</dd>
1317
+ <dt><a href="docs/v2/">v2.x</a></dt>
1318
+ <dd>Support for IE < 11 had to be dropped in v2 because it required many hacks and was holding back
1319
+ other browsers. Otherwise, same support than v1.</dd> <dt><a href="docs/v3/">v3.x</a></dt>
1320
+ <dd>ObjectModel v3 is built around ES6 Proxies, so requires modern environments :
1321
+ Edge 14+, Firefox 47+, Chrome 50+, Safari 10+, Node 6.0+.</dd>
1322
+ <dt>v4.x</dt>
1323
+ <dd>Support for Node 6-7 has been dropped in v4. Use a transpiler like Babel to target the same
1324
+ environments than v3.</dd>
1325
+ </dl>
1326
+ <p>More information about these changes between major versions on the
1327
+ <a href="https://github.com/sylvainpolletvillard/ObjectModel/releases" target="_blank"
1328
+ rel="noopener">
1329
+ Github Releases</a> page</p>
1330
+ </details>
1331
+
1332
+ <details>
1333
+ <summary>What is the impact on performance ?</summary>
1334
+ <p>To get dynamic type validation, Object models have to use Proxies to catch properties assignments.
1335
+ This has a performance cost, especially on older browsers. Therefore, it is not advisable to use
1336
+ object models in performance-critical parts of your applications. In particular, Array models and
1337
+ circular references in models have the most impact on performance. But in general, the loss of time
1338
+ does not exceed a few milliseconds and is quite negligible.</p>
1339
+ </details>
1340
+
1341
+ <details>
1342
+ <summary>How can I get the model from an instance ?</summary>
1343
+ <div class="grid doc-code">
1344
+ <div class="doc">
1345
+ <p>With the <code>constructor</code> property. If this property is used in your model, you can
1346
+ also retrieve it with <code>Object.getPrototypeOf(instance).constructor</code>. This is
1347
+ useful for retrieving the type of a property for example.</p>
1348
+ </div>
1349
+ <div class="panel">
1350
+ <pre><code class="language-javascript">const User = ObjectModel({ name: String }),
1351
+ joe = User({ name: "Joe" });
1352
+
1353
+ const modelOfJoe = joe.constructor // or Object.getPrototypeOf(joe).constructor;
1354
+ // modelOfJoe === User
1355
+ // modelOfJoe.definition.name === String</code></pre>
1356
+ </div>
1357
+ </div>
1358
+ </details>
1359
+
1360
+ <details>
1361
+ <summary>
1362
+ How do I declare a constructor function to be called on instanciation before validating my model ?
1363
+ </summary>
1364
+ <div class="grid doc-code">
1365
+ <div class="doc">
1366
+ <p>The recommended way is to use a factory function to instanciate your models. You can declare
1367
+ as many different factories as needed, which makes this pattern both simple and flexible.
1368
+ </p>
1369
+ </div>
1370
+ <div class="panel">
1371
+ <pre><code class="language-javascript">const User = ObjectModel({
1372
+ firstName: String,
1373
+ lastName: String,
1374
+ fullName: String
1375
+ });
1376
+
1377
+ User.create = function(properties){
1378
+ properties.fullName = properties.firstName + " " + properties.lastName;
1379
+ return new User(properties);
1380
+ };
1381
+
1382
+ const joe = User.create({ firstName: "Joe", lastName: "Dalton" });</code></pre>
1383
+ </div>
1384
+ </div>
1385
+ </details>
1386
+
1387
+ <details>
1388
+ <summary>Is there a way to compute default values ?</summary>
1389
+ <div class="grid doc-code">
1390
+ <div class="doc">
1391
+ <p>Computed default values are not really default values in the sense that these values are
1392
+ different from one instance to another, and computed at instanciation time. On the other
1393
+ hand, this computation logic is the same for all instances. So this computation logic should
1394
+ be declared as a getter/setter in the model prototype.</p>
1395
+ <p>One could have different expectations regarding whether these values are computed <i>once at instanciation</i> or <i>at every property read</i>. But this <i>once at instanciation</i> behaviour can also be implemented with a getter using another undeclared private property as cache, as shown in this code example.</p>
1396
+ <p>See <a href="https://github.com/sylvainpolletvillard/ObjectModel/issues/107" target="_blank"
1397
+ rel="external">issue #107</a> for more information.</p>
1398
+ </div>
1399
+ <div class="panel">
1400
+ <pre><code class="language-javascript">const T = new ObjectModel({
1401
+ id: String
1402
+ });
1403
+
1404
+ T.prototype = {
1405
+ get id() {
1406
+ if(this._id === undefined) this._id = generateUID()
1407
+ return this._id
1408
+ },
1409
+ set id(newId){
1410
+ this._id = newId
1411
+ }
1412
+ };</code></pre>
1413
+ </div>
1414
+ </div>
1415
+ </details>
1416
+
1417
+ <details>
1418
+ <summary>How do I prevent adding undeclared properties to my object models ?</summary>
1419
+ <p>This feature, previously known as <b>sealed models</b>, has been removed from the library since v4.x.
1420
+ It is now available as a custom model
1421
+ <a target="_blank" href="docs/examples/sealed.js">available here</a>.
1422
+ </p>
1423
+ </details>
1424
+
1425
+ <details>
1426
+ <summary>How should I deal with circular references in my model definitions ?</summary>
1427
+ <div class="grid doc-code">
1428
+ <div class="doc">
1429
+ <p>You can't refer to a model or instance that is not yet defined, so you have to update the
1430
+ definition afterwards:</p>
1431
+ </div>
1432
+ <div class="panel">
1433
+ <pre><code class="language-javascript">const Honey = ObjectModel({
1434
+ sweetie: undefined // Sweetie is not yet defined
1435
+ });
1436
+
1437
+ const Sweetie = ObjectModel({
1438
+ honey: Honey
1439
+ });
1440
+
1441
+ Honey.definition.sweetie = [Sweetie];
1442
+
1443
+ const joe = Honey({ sweetie: undefined }); // ann is not yet defined
1444
+ const ann = Sweetie({ honey: joe });
1445
+ joe.sweetie = ann;</code></pre>
1446
+ </div>
1447
+ </div>
1448
+ </details>
1449
+
1450
+ <details>
1451
+ <summary>How can I serialize/deserialize objects while preserving type information ?</summary>
1452
+ <div class="grid doc-code-code">
1453
+ <div class="doc">
1454
+ <p>Serializing in JSON necessarily implies that you lose the type information, except if you
1455
+ store it manually with your data, then retrieve it with a custom parsing function. It is for
1456
+ the best to let you decide how you want to store the type information within your data.</p>
1457
+ <p>Here is a proposal of implementation using a simple <code>{ _value, _type }</code> wrapper:
1458
+ </p>
1459
+ </div>
1460
+
1461
+ <div class="panel panel1">
1462
+ <span class="legend">Implementation</span>
1463
+ <pre><code class="language-javascript">Model.prototype.serialize = function(instance, models){
1464
+ const names = Object.keys(models);
1465
+ return JSON.stringify(instance, function(key, value){
1466
+ const modelName = names.find(name => value instanceof models[name]);
1467
+ if(modelName && key !== "_value"){
1468
+ return { _type: modelName, _value: value }
1469
+ }
1470
+ return value;
1471
+ }, '\t');
1472
+ }
1473
+
1474
+ Model.prototype.parse = function(json, models){
1475
+ return JSON.parse(json, function(key, o){
1476
+ if(o && o._type in models){
1477
+ return new models[o._type](o._value);
1478
+ }
1479
+ return o;
1480
+ })
1481
+ }</code></pre>
1482
+ </div>
1483
+ <div class="panel panel2">
1484
+ <span class="legend">Example</span>
1485
+ <pre><code class="language-javascript">const Type1 = ObjectModel({ content: String }).defaultTo({ content: 'Content 1' }),
1486
+ Type2 = ObjectModel({ content: String }).defaultTo({ content: 'Content 2' }),
1487
+ Container = ObjectModel({ items: ArrayModel([Type1, Type2]) });
1488
+
1489
+ // List all your serializable models here
1490
+ const serializableModels = { Container, Type1, Type2 };
1491
+
1492
+ let a = new Container({ items: [new Type1, new Type2] });
1493
+
1494
+ let json = Container.serialize(a, serializableModels);
1495
+ console.log(json);
1496
+
1497
+ let b = Container.parse(json, serializableModels);
1498
+ console.log(
1499
+ b instanceof Container,
1500
+ b.items[0] instanceof Type1,
1501
+ b.items[1] instanceof Type2
1502
+ );</code></pre>
1503
+ </div>
1504
+ </div>
1505
+ </details>
1506
+
1507
+ <details>
1508
+ <summary>Is it possible to convert TypeScript/Flow annotations to Models ?</summary>
1509
+ <p>It may be possible with Babel, but ObjectModel is not the best option for this purpose.</p>
1510
+ <p>Models have been designed to use all the advantages of runtime validation, such as complex
1511
+ assertions, mixed value/type checking or custom error collectors. Compared to static type systems,
1512
+ the feature set is really different. I suggest you to combine the strengths of static and dynamic
1513
+ type-checking: use static annotations in your business logic layer, and Models at your API
1514
+ boundaries to validate user input, network responses or serialized data. You can also use both at
1515
+ the same place as demonstrated in the introduction video.</p>
1516
+ <p>If you are looking for a runtime type-checking solution that acts as a fully transparent layer over
1517
+ an existing static type system, you should try
1518
+ <a href="https://codemix.github.io/flow-runtime/" target="_blank" rel="noopener">flow-runtime</a>
1519
+ instead.</p>
1520
+ </details>
1521
+
1522
+ <details>
1523
+ <summary>How do I validate values returned by Promises or other async structures ?</summary>
1524
+
1525
+ <div class="grid doc-code-code">
1526
+ <div class="doc">
1527
+ <p>It is impossible to validate both the Promise and the resolved value at the same time,
1528
+ because of its asynchronous nature. So it actually requires two different models: a first
1529
+ one to check if it is actually a Promise, then a second one to validate the emitted value
1530
+ once the promise is resolved. See
1531
+ <a href="https://github.com/sylvainpolletvillard/ObjectModel/issues/88" target="_blank"
1532
+ rel="noopener">this issue</a>
1533
+ for details.</p>
1534
+ </div>
1535
+
1536
+ <div class="panel panel1">
1537
+ <span class="legend">Example helper for Promises</span>
1538
+ <pre><code class="language-javascript">const PromiseOf = definition => {
1539
+ const PromiseModel = BasicModel(Promise);
1540
+ const ResolvedValueModel = Model(definition)
1541
+ return p => PromiseModel(p).then(x => ResolvedValueModel(x))
1542
+ }</code></pre>
1543
+ </div>
1544
+ <div class="panel panel2">
1545
+ <span class="legend">Usage</span>
1546
+ <pre><code class="language-javascript">let p = new Promise(resolve => setTimeout(() => resolve(42), 1000));
1547
+
1548
+ PromiseOf(Number)(p).then(n => {
1549
+ // ObjectModel has validated both the Promise and the resolved value
1550
+ })</code></pre>
1551
+ </div>
1552
+ </div>
1553
+ </details>
1554
+
1555
+ </section>
1556
+
1557
+ <section>
1558
+ <h2>I have a question / suggestion / bug to report</h2>
1559
+ <p>Please check the documentation twice, then open an issue on the
1560
+ <a href="https://github.com/sylvainpolletvillard/ObjectModel/issues" target="_blank"
1561
+ rel="noopener">Github repository</a>
1562
+ </p>
1563
+ <p>
1564
+ You can also ask for support on the
1565
+ <a href="https://gitter.im/sylvainpolletvillard/ObjectModel" target="_blank" rel="noopener">
1566
+ Gitter channel
1567
+ </a>.
1568
+ </p>
1569
+ </section>
1570
+
1571
+ <section>
1572
+ <h2>Like what you see ? Share it with the world !</h2>
1573
+
1574
+ <ul class="share-buttons">
1575
+ <li>
1576
+ <a href="https://www.facebook.com/sharer/sharer.php?u=http%3A%2F%2Fobjectmodel.js.org&t=ObjectModel%3A%20Model%20Definition%20and%20Runtime%20Type%20Checking%20for%20JavaScript"
1577
+ title="Share on Facebook" target="_blank" rel="noopener"
1578
+ onclick="window.open(this.href); return false;">
1579
+ <img class="lazy" alt="Facebook" data-src="docs/res/facebook.svg" width="48" height="48">
1580
+ </a>
1581
+ </li>
1582
+ <li>
1583
+ <a href="https://twitter.com/intent/tweet?source=http%3A%2F%2Fobjectmodel.js.org&text=ObjectModel%3A%20Model%20Definition%20and%20Runtime%20Type%20Checking%20for%20JavaScript%20-%20http%3A//objectmodel.js.org"
1584
+ target="_blank" rel="noopener" title="Tweet" onclick="window.open(this.href); return false;">
1585
+ <img class="lazy" alt="Twitter" data-src="docs/res/twitter.svg" width="48" height="48">
1586
+ </a>
1587
+ </li>
1588
+ <li>
1589
+ <a href="https://gitter.im/sylvainpolletvillard/ObjectModel?utm_source=website" target="_blank"
1590
+ rel="noopener" title="Chat on Gitter">
1591
+ <img class="lazy" alt="Gitter" data-src="docs/res/gitter.svg" width="48" height="48">
1592
+ </a>
1593
+ </li>
1594
+ </ul>
1595
+
1596
+ </section>
1597
+
1598
+ <hr>
1599
+ <footer><a href="LICENSE">MIT licensed</a> - Sylvain Pollet-Villard</footer>
1600
+ </div>
1601
+
1602
+ </body>
1603
+
1606
1604
  </html>