@expressive/mvc 0.17.1 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md DELETED
@@ -1,1666 +0,0 @@
1
- <br/>
2
-
3
- <p align="center">
4
- <img height="90" src="./assets/logo.svg" alt="Expressive Logo"/>
5
- <h1 align="center">
6
- Expressive MVC
7
- </h1>
8
- </p>
9
-
10
- <h4 align="center">
11
- Accessible control for anywhere and everywhere in React apps
12
- </h4>
13
-
14
- <p align="center">
15
- <a href="https://www.npmjs.com/package/@expressive/mvc"><img alt="NPM" src="https://badge.fury.io/js/%40expressive%2Fmvc.svg"></a>
16
- <a href=""><img alt="Build" src="https://shields-staging.herokuapp.com/npm/types/@expressive/mvc.svg"></a>
17
- <img src="https://img.shields.io/badge/Coverage-100%25-brightgreen.svg">
18
- </p>
19
-
20
- <p align="center">
21
- Define classes to power UI by extending <code>Model</code>.<br/>
22
- Builtin hooks will manage renders, as needed, for any data.<br/>
23
- When properties change, your components will too.<br/>
24
- </p>
25
-
26
- <br/>
27
- <p align="center">
28
- <b>Have suggestions, need help or just want to discuss?</b><br/><br/>
29
- <a href="https://join.slack.com/t/expressivejs/shared_invite/zt-s2j5cdhz-gffKn3bTATMbXf~iq4pvHg" alt="Join Slack">
30
- <img src="https://img.shields.io/badge/Slack-Come%20say%20hi!-blueviolet" />
31
- </a>
32
- </p>
33
-
34
- <br/>
35
- <h1 id="overview-section">Overview</h1>
36
-
37
- Classes which extend `Model` can manage behavior for components. Custom models are easy to define, use and even provide. While hooks are great, state and logic are not a part of well-formed JSX. Models help wrap that stuff in robust, portable controllers instead.
38
-
39
-
40
- <br/>
41
-
42
- ## How to use
43
-
44
- ### Step 1
45
-
46
- Create a class to extend `Model` and shape it to your liking.
47
-
48
- ```js
49
- class Counter extends Model {
50
- current = 1
51
-
52
- increment = () => { this.current++ };
53
- decrement = () => { this.current-- };
54
- }
55
- ```
56
-
57
- ### Step 2
58
-
59
- Pick a built-in hook, such as `use()`, to make a component stateful.
60
-
61
- ```jsx
62
- const KitchenCounter = () => {
63
- const { current, increment, decrement } = Counter.use();
64
-
65
- return (
66
- <div>
67
- <button onClick={decrement}>{"-"}</button>
68
- <pre>{current}</pre>
69
- <button onClick={increment}>{"+"}</button>
70
- </div>
71
- )
72
- }
73
- ```
74
-
75
- ### Step 3
76
- [See it in action](https://codesandbox.io/s/example-counter-th8xl) 🚀 You've already got something usable!
77
-
78
- <br/>
79
- <h2 id="good-start">Things you can do</h2>
80
-
81
- ### Track any number of values
82
- Updating any of them will trigger a render.
83
-
84
- ```jsx
85
- class State extends Model {
86
- a = 1;
87
- b = 2;
88
- c = 3;
89
- }
90
-
91
- const MyComponent = () => {
92
- const { a, b, c } = State.use();
93
-
94
- return <div>...</div>
95
- }
96
- ```
97
- <br/>
98
-
99
- ### Refresh using simple assignment
100
- Reserved `set` loops back to the instance, to update from inside a component.
101
-
102
- ```jsx
103
- class Hello extends Model {
104
- name = "World";
105
- }
106
-
107
- const MyComponent = () => {
108
- const { name, set } = Hello.use();
109
-
110
- return (
111
- <p onClick={() => set.name = "Foobar"}>
112
- Hello {name}!
113
- </p>
114
- )
115
- }
116
- ```
117
- <br/>
118
-
119
- ### Share state via context
120
- Provide and consume with simple methods. Classes are their own key.
121
-
122
- ```jsx
123
- import Model, { Provider } from "@expressive/mvc";
124
-
125
- class Shared extends Model {
126
- value = 1;
127
- }
128
-
129
- const Parent = () => (
130
- <Provider of={Shared}>
131
- <Child />
132
- </Provider>
133
- )
134
-
135
- const Child = () => {
136
- const { value } = Shared.get();
137
-
138
- return (
139
- <p>Shared value is: {value}!</p>
140
- )
141
- }
142
- ```
143
- <br/>
144
-
145
- ### Respond to lifecycle
146
- Hooks aware of lifecycle will call method-handlers you define.
147
-
148
- ```jsx
149
- class Timer extends Model {
150
- interval = undefined;
151
- elapsed = 1;
152
-
153
- componentDidMount(){
154
- const inc = () => this.elapsed++;
155
- this.interval = setInterval(inc, 1000);
156
- }
157
-
158
- componentWillUnmount(){
159
- clearInterval(this.interval);
160
- }
161
- }
162
-
163
- const MyTimer = () => {
164
- const { elapsed } = Timer.use();
165
-
166
- return <p>I've existed for { elapsed } seconds!</p>;
167
- }
168
- ```
169
- <br/>
170
-
171
- ### Control components with `async`
172
-
173
- ```jsx
174
- class Greetings extends Model {
175
- response = undefined;
176
- waiting = false;
177
- error = false;
178
-
179
- sayHello = async () => {
180
- this.waiting = true;
181
-
182
- try {
183
- const res = await fetch("https://my.api/hello");
184
- this.response = await res.text();
185
- }
186
- catch(){
187
- this.error = true;
188
- }
189
- }
190
- }
191
-
192
- const MyComponent = () => {
193
- const { error, response, waiting, sayHello } = Greetings.use();
194
-
195
- if(response)
196
- return <p>Server said: {response}</p>
197
-
198
- if(error)
199
- return <p>There was an issue saying hello</p>
200
-
201
- if(waiting)
202
- return <p>Sent! Waiting on response...</p>
203
-
204
- return (
205
- <a onClick={sayHello}>Say hello to server!</a>
206
- )
207
- }
208
- ```
209
- <br/>
210
-
211
- ### Extend to configure
212
- Use class extension to set needed properties.
213
- Reusable logic is easy to implement, document and share.
214
-
215
- ```ts
216
- abstract class About extends Model {
217
- abstract name: string;
218
- abstract birthday: Date;
219
- abstract wants: any;
220
-
221
- happyBirthday(){
222
- // ...
223
- }
224
- }
225
-
226
- class AboutMe extends About {
227
- name = "John Doe";
228
- birthday = new Date("January 1");
229
- wants = "a PS5";
230
- }
231
- ```
232
-
233
- </br>
234
-
235
- #### And you can do more, all with type safety and code-assist, out of the box.
236
-
237
- </br>
238
-
239
- # Contents
240
-
241
- <!-- Manual bullets because I don't like default <li> -->
242
-
243
- ### Introductions
244
- &ensp; •&nbsp; **[Install and Import](#install-section)** <br/>
245
-
246
- **Getting Started** <br/>
247
- &ensp; •&nbsp; [The basics](#concept-simple) <br/>
248
- &ensp; •&nbsp; [Destructuring](#concept-destruct) <br/>
249
- &ensp; •&nbsp; [Methods](#concept-method) <br/>
250
- &ensp; •&nbsp; [Getters](#concept-getters) <br/>
251
- &ensp; •&nbsp; [Constructor](#concept-constructor) <br/>
252
- &ensp; •&nbsp; [Applied Props](#method-uses) <br/>
253
-
254
- **Dynamics** <br/>
255
- &ensp; •&nbsp; [Lifecycle](#concept-lifecycle) <br/>
256
- &ensp; •&nbsp; [Events](#concept-events) <br/>
257
- &ensp;&ensp;&ensp; ◦&nbsp; [Listening](#concept-listen-event) <br/>
258
- &ensp;&ensp;&ensp; ◦&nbsp; [Dispatch](#concept-push-event) <br/>
259
- &ensp;&ensp;&ensp; ◦&nbsp; [Built-in](#concept-builtin-events) <br/>
260
- &ensp; •&nbsp; [Monitored Props](#concept-external) <br/>
261
- &ensp; •&nbsp; [Async and callbacks](#concept-async) <br/>
262
-
263
- **Shared State** <br/>
264
- &ensp; •&nbsp; [Basics](#concept-sharing) <br/>
265
- &ensp; •&nbsp; [Provider](#concept-provider) <br/>
266
- &ensp;&ensp;&ensp; ◦&nbsp; [Spawning](#concept-provider-spawning) <br/>
267
- &ensp;&ensp;&ensp; ◦&nbsp; [With props](#concept-provider-props) <br/>
268
- &ensp;&ensp;&ensp; ◦&nbsp; [Multiple](#concept-provider-multi) <br/>
269
- &ensp; •&nbsp; [Globals](#section-global) <br/>
270
- &ensp;&ensp;&ensp; ◦&nbsp; [Setup](#section-singleton-activate) <br/>
271
-
272
- **Consuming** <br/>
273
- &ensp; •&nbsp; [Hooks](#concept-consumer-hooks) <br/>
274
- &ensp;&ensp;&ensp; ◦&nbsp; [`get`](#method-get) <br/>
275
- &ensp;&ensp;&ensp; ◦&nbsp; [`tap`](#method-tap) <br/>
276
- &ensp; •&nbsp; [Consumer](#concept-consumer) <br/>
277
- &ensp;&ensp;&ensp; ◦&nbsp; [children](#consumer-render) <br/>
278
- &ensp;&ensp;&ensp; ◦&nbsp; [`get`](#consumer-get) <br/>
279
- &ensp;&ensp;&ensp; ◦&nbsp; [`tap`](#consumer-tap) <br/>
280
- &ensp;&ensp;&ensp; ◦&nbsp; [`has`](#consumer-has) <br/>
281
-
282
- **Composition** <br/>
283
- &ensp; •&nbsp; [Simple Composition](#concept-compose) <br/>
284
- &ensp; •&nbsp; [Ambient Controllers](#concept-ambient) <br/>
285
- &ensp; •&nbsp; [Child Controllers](#concept-child-model) <br/>
286
- &ensp; •&nbsp; [Parent Controller](#concept-parent-model) <br/>
287
-
288
- <!-- **Customizing** <br/>
289
- &ensp; •&nbsp; [Extending Custom Models](#concept-extension) <br/>
290
- &ensp; •&nbsp; [Impact on Context](#concept-extension-context) <br/>
291
- &ensp; •&nbsp; [Instructions](#concept-instruction) <br/>
292
- &ensp; •&nbsp; [Custom Hooks](#concept-custom-hooks) <br/>
293
- &ensp; •&nbsp; [Using Meta](#concept-meta) <br/> -->
294
-
295
- **Best Practices** <br/>
296
- &ensp; •&nbsp; [Documenting and Types](#concept-typescript) <br/>
297
-
298
- ### Concepts
299
- &ensp; •&nbsp; [Subscriptions](#concept-lazy) <br/>
300
- &ensp; •&nbsp; [Auto Debounce](#concept-debounce) <br/>
301
- <!-- &ensp; •&nbsp; [Selectors](#concept-selectors) <br/> -->
302
-
303
- <!-- ### API
304
- &ensp; •&nbsp; [Model](#api-controller) <br/>
305
- &ensp; •&nbsp; [Singleton](#api-singleton) <br/>
306
- &ensp; •&nbsp; [Reserved](#api-reserved) <br/>
307
- &ensp; •&nbsp; [Lifecycle](#api-lifecycle) <br/> -->
308
-
309
- <br/>
310
-
311
- <h1 id="install-section">Installation</h1>
312
-
313
- Install with your preferred package manager
314
- ```bash
315
- npm install --save @expressive/mvc
316
- ```
317
-
318
- Import and use in your react apps
319
-
320
- ```js
321
- import Model from "@expressive/mvc";
322
- ```
323
-
324
- <br/><br/>
325
- <h1 id="started-section">Getting Started</h1>
326
-
327
- Ultimately, the workflow is simple.
328
-
329
- 1. Create a class. Fill it with the values, getters, and methods you'll need.
330
- 2. Extend `Model` (or any derivative, for that matter), making it reactive.
331
- 3. Within a component, use built-in methods as you would normal hooks.
332
- 4. Destructure out the values used by a component, to then subscribe.
333
- 5. Update those values on demand. Component will sync automagically. ✨
334
- <br/>
335
-
336
- > You may recognize this as similar to approach pioneered by [MobX](https://mobx.js.org/README.html). Here though, we avoid using decorators and other boilerplate.
337
-
338
- ### Glossary
339
-
340
- - **Model**: Any class you'll write extending `Model`. Defines a type of state.
341
- - **Controller**: An instance of Model, containing its state and behavior.
342
- - **State**: The values managed by a controller, as defined by the model.
343
- - **Subscriber**: A hook, or otherwise a callback, responding to updates.
344
- - **View**: A function-component which accepts hooks, to contain a controller.
345
- - **Element**: Instance of a component mounted, having state and lifecycle.
346
-
347
- <br/>
348
- <h2 id="concept-simple">First Steps</h2>
349
-
350
- Start with a bare minimum:
351
-
352
- ```jsx
353
- import Model from "@expressive/mvc";
354
-
355
- class Counter extends Model {
356
- number = 1
357
- }
358
- ```
359
-
360
- Define a model with any number of properties needed. Values set in constructor (or class properties) are the initial state.
361
-
362
- ```js
363
- import { Plus, Minus } from "./components";
364
-
365
- const KitchenCounter = () => {
366
- const state = Counter.use();
367
-
368
- return (
369
- <div>
370
- <Minus onClick={() => state.number -= 1} />
371
- <pre>{ state.number }</pre>
372
- <Plus onClick={() => state.number += 1} />
373
- </div>
374
- )
375
- }
376
- ```
377
-
378
- One of the static-methods on your class will be `use`. This is a hook; it will create a new instance of your model and attach it to a component.
379
-
380
- Now, we can edit instance properties as we see fit. When values change, our hook calls for a refresh as needed.
381
-
382
- <br/>
383
-
384
- <h2 id="concept-destruct">Destructuring</h2>
385
-
386
- [Model hooks are lazy](#concept-lazy), and will only update for values seen to be in-use.
387
-
388
- A good idea is to destructure values. This keeps intent clear, and avoids unexpected behavior.
389
-
390
- After destructuring though, we still might need access to the full state. For this, a `get` and `set` property are defined as a loop-back.
391
-
392
- ### `set`
393
-
394
- Mainly for assigning new values to state.
395
-
396
- ```jsx
397
- const KitchenCounter = () => {
398
- const { number, set } = Counter.use();
399
-
400
- return (
401
- <div>
402
- <Minus onClick={() => set.number -= 1} />
403
- <pre>{ number }</pre>
404
- <Plus onClick={() => set.number += 1} />
405
- </div>
406
- )
407
- }
408
- ```
409
- <!-- <sup><a href="https://codesandbox.io/s/example-event-vsmib">View in CodeSandbox</a></sup> -->
410
-
411
- > `set.number` See what we did there? 🤔
412
-
413
- ### `get`
414
-
415
- When an observable value is accessed, the controller assumes a refresh is needed anytime that property changes. In plenty of situations, this isn't the case, so `get` serves as an escape-hatch.
416
-
417
- ```js
418
- const { get: state, value } = Model.use();
419
- ```
420
- > Is a nicer (and more stable) way of saying:
421
- ```js
422
- const state = Model.use();
423
- const { value } = state;
424
- ```
425
- <br/>
426
-
427
- > **Good to know:** <br/>
428
- > Reason this works is because returned `state` is a proxy, there to spy on property access. On the other hand, `get` and `set` are a direct reference to state.
429
-
430
- <br/>
431
-
432
- <h2 id="concept-method">Adding methods</h2>
433
-
434
- What's a class without some methods? Defining "actions" can help delineate state-change.
435
-
436
- ```jsx
437
- class Counter extends Model {
438
- current = 1
439
-
440
- // Using arrow functions, to preserve `this`.
441
- // In-practice, this acts a lot like a useCallback, but better!
442
- increment = () => { this.current++ };
443
- decrement = () => { this.current-- };
444
- }
445
- ```
446
- With this, you can write even the most complex components, all while maintaining key benefits of a functional-component. Much easier on the eyeballs.
447
-
448
- ```jsx
449
- const KitchenCounter = () => {
450
- const { current, decrement, increment } = Counter.use();
451
-
452
- return (
453
- <Row>
454
- <Minus onClick={decrement} />
455
- <pre>{ current }</pre>
456
- <Plus onClick={increment} />
457
- </Row>
458
- )
459
- }
460
- ```
461
- <sup><a href="https://codesandbox.io/s/example-counter-th8xl">View in CodeSandbox</a></sup>
462
- <br/>
463
-
464
- > **Good to know:** <br/>
465
- > While obviously cleaner, this is also more efficient than inline-functions. Not only do actions have names, we avoid a new closure for every render. This can save a lot of unnecessary rendering downstream, for assumed "new" callbacks. 😬
466
- <!-- <sup><a href="https://codesandbox.io/s/example-actions-1dyxg">View in CodeSandbox</a></sup> -->
467
-
468
- <br/>
469
-
470
- <h2 id="concept-getters">Computed Values</h2>
471
-
472
- State may contain derivative values too, simply define get-methods on your model. They will be managed by the controller automatically.
473
-
474
- Through the same mechanism as hooks, getters know when the specific properties they access are updated. Whenever that happens, they rerun. If a new value is returned, the update is paid forward.
475
-
476
- ```js
477
- const { floor } = Math;
478
-
479
- class Timer extends Model {
480
- constructor(){
481
- super();
482
- setInterval(() => this.seconds++, 1000);
483
- }
484
-
485
- seconds = 0;
486
-
487
- get minutes(){
488
- return floor(this.seconds / 60);
489
- }
490
-
491
- get hours(){
492
- // getters can subscribe to other getters as well 🤙
493
- return floor(this.minutes / 60);
494
- }
495
-
496
- get format(){
497
- const hrs = this.hours;
498
- const min = this.minutes % 60;
499
- const sec = this.seconds % 60;
500
-
501
- return `${hrs}:${min}:${sec}`;
502
- }
503
- }
504
- ```
505
- <sup><a href="https://codesandbox.io/s/example-timer-torjc">View in CodeSandbox</a></sup>
506
-
507
- A getter's value is cached, facing the user. It will only run when a dependency changes, and **not upon access** as you might expect. However, getters are also lazy, so first access is one exception.
508
-
509
- Getters run whenever a controller thinks they *could* change. Make sure to design them along these lines:
510
- - Getters should be deterministic. Only expect a change where inputs have changed.
511
- - Avoid computing from values which change a lot, but don't change output as often.
512
- - [Goes without saying](https://www.youtube.com/watch?v=0i0IlSKn0sE "Goes Without Saying") but, **side-effects are an anti-pattern** and may cause infinite loops.
513
-
514
- <br/>
515
-
516
- <h2 id="concept-constructor">Constructor arguments</h2>
517
-
518
- The method `use(...)`, while building an instance, will pass its arguments to the constructor. This helps in reusing models, being able to customize the initial state.
519
-
520
- > Typescript
521
- ```ts
522
- class Greetings extends Model {
523
- firstName: string;
524
- surname: string;
525
-
526
- constructor(fullName: string){
527
- super();
528
-
529
- const [ first, last ] = fullName.split(" ");
530
-
531
- this.firstName = first;
532
- this.surname = last;
533
- }
534
- }
535
- ```
536
- ```jsx
537
- const SayHello = ({ fullName }) => {
538
- const { firstName } = Greetings.use(fullName);
539
-
540
- return <b>Hello {firstName}!</b>;
541
- }
542
- ```
543
-
544
- <br/>
545
-
546
- <h2 id="concept-passing-props">Assigning props to state</h2>
547
-
548
- Besides `use`, similar methods will assign values after a controller is created. Good alternative to a manual setup.
549
-
550
- <h4 id="method-uses"><code>Model.uses(source, keys?)</code></h4>
551
-
552
- ```js
553
- class Greetings extends Model {
554
- name = undefined;
555
- birthday = undefined;
556
-
557
- get firstName(){
558
- return this.name.split(" ")[0];
559
- }
560
-
561
- get isBirthday(){
562
- const td = new Date();
563
- const bd = new Date(this.birthday);
564
-
565
- return (
566
- td.getMonth() === bd.getMonth() &&
567
- td.getDate() === bd.getDate()
568
- )
569
- }
570
- }
571
- ```
572
- Greetings defines *firstName* & *isBirthday*; however they themselves depend on *name* and *birthday*, which start out undefined. We can pass in `props` to help with that.
573
-
574
- ```jsx
575
- const HappyBirthday = (props) => {
576
- const { firstName, isBirthday } = Greetings.uses(props);
577
-
578
- return (
579
- <div>
580
- <span>Hi {firstName}</span>
581
- {isBirthday
582
- ? <b>, happy birthday!</b>
583
- : <i>, happy unbirthday! {"🎩"}</i>
584
- }
585
- </div>
586
- );
587
- };
588
- ```
589
-
590
- Now, we can define these props from outside!
591
- ```jsx
592
- const SayHello = () => (
593
- <HappyBirthday
594
- name="John Doe"
595
- birthday="January 1"
596
- />
597
- )
598
- ```
599
- <sup><a href="https://codesandbox.io/s/example-uses-75mvm">View in CodeSandbox</a></sup>
600
-
601
- This method is naturally picky and will only capture values pre-defined, as `undefined` or otherwise. You can however, specify which properties to pull from, like so:
602
-
603
- ```js
604
- const state = Model.uses({ ... }, ["name", "birthday"]);
605
- ```
606
-
607
- This way, objects containing _more_ than expect data may be used, without collisions or pollution of state.
608
-
609
- <br/>
610
-
611
- ### ✅ Level 1 Clear!
612
- > In this chapter we've learned the basics, of how to create and utilize a custom state. For most people who just want smarter components, this could be enough! However, we can go well beyond making just a fancy hook.
613
-
614
- <br/>
615
-
616
- <h1 id="managing-section">Creating a dynamic state</h1>
617
-
618
- So far, all our examples have been passive. Models can serve a bigger roll, however, to evolve state even without user input.
619
-
620
- Because state is a portable object, we can modify it from anywhere, and more-crucially whenever. This makes async things pretty low maintenance. You implement the business-logic, controllers will handle the rest.
621
-
622
- Here are a few ways to smarten up your controllers:
623
- <br/><br/>
624
-
625
- <h2 id="concept-lifecycle">Lifecycle</h2>
626
-
627
- Built-in hooks, besides receiving updates, can dispatch events related the component they're a part of. On the model, reserved methods may be defined to handle these events.
628
-
629
- ```jsx
630
- class TimerControl extends Model {
631
- interval = undefined;
632
- elapsed = 1;
633
-
634
- componentDidMount(){
635
- const inc = () => this.elapsed++;
636
- this.interval = setInterval(inc, 1000);
637
- }
638
-
639
- componentWillUnmount(){
640
- clearInterval(this.interval);
641
- }
642
- }
643
- ```
644
- > Where have I seen this before?... 🤔
645
- ```jsx
646
- const MyTimer = () => {
647
- const { elapsed } = TimerControl.use();
648
-
649
- return <pre>I've existed for { elapsed } seconds!</pre>;
650
- }
651
- ```
652
-
653
- <sup><a href="https://codesandbox.io/s/example-component-lifecycle-8xx5c">View in CodeSandbox</a></sup>
654
-
655
- > We will [learn more](#lifecycle-api) about Lifecycle events in a later section.
656
-
657
- <br />
658
-
659
- <h2 id="concept-events">Event Handling</h2>
660
-
661
- Besides state change, what a subscriber really cares about is events. *Updates are just one source for an event.* When any property gains a new value, subscribers are simply notified and act accordingly.
662
-
663
- > **Good to know:** <br/>
664
- > All changes to state occure in batches, called frames. Whenever state receives a new value, a zero-second timer starts. Anything which updates before the commit-phase is considered "reasonably syncronous" and included. After that, events are then fired, and controller resets for the next event.
665
-
666
- <br />
667
-
668
- <h2 id="concept-listen-event">Listening for events manually</h2>
669
-
670
- ```js
671
- const normalCallback = (value, key) => {
672
- console.log(`${key} was updated with ${value}!`)
673
- }
674
-
675
- const squashCallback = (keys) => {
676
- console.log(`Update did occure for: ${keys}!`)
677
- }
678
- ```
679
-
680
- > By default, callbacks are run once for every key/value update, even if multiple happen at the same time. If you'd rather know when an event happens for *any* key, squash will fire once for a frame, with a list of keys updated.
681
-
682
- #### `state.on(key, callback, squash?, once?) => remove`
683
-
684
- Register a new listener for given key(s). `callback` will be fired when `state[key]` updates, or a synthetic event is sent.
685
-
686
- The method also returns a callback; use it to stop subscribing.
687
-
688
- #### `state.once(key, callback, squash?) => cancel`
689
-
690
- Will also expire when triggered. May be cancelled with the returned callback.
691
-
692
- #### `state.once(key) => Promise<keys[]>`
693
-
694
- If `callback` is not provided, `once` will return a Promise of keys watched/updated.
695
-
696
- <br />
697
- <h2 id="concept-listen-effect">Defining an effect</h2>
698
-
699
- ```js
700
- function effectCallback(state){
701
- this === state; // true
702
-
703
- console.log(`Update did occure in one or more watched values.`)
704
-
705
- return () =>
706
- console.log(`Another update will occure, cleanup may be done here.`)
707
- }
708
- ```
709
-
710
- #### `state.effect(effectCallback, keys) => remove`
711
-
712
- A more versatile method used to monitor one or more properties with the same callback. Optionally, that callback may return a handler to clean-up, when the process repeats.
713
-
714
- <br/>
715
-
716
- ```js
717
- function subscribedCallback(state){
718
- const { name } = this; // or `state`
719
-
720
- console.log(`Hello ${name}`)
721
-
722
- return () =>
723
- console.log(`Goodbye ${name}`)
724
- }
725
- ```
726
- #### `state.effect(subscribedCallback) => remove`
727
-
728
- If no explicit `keys` are given, the effect callback will self-subscribe. Just like a hook, it detects values used and automatically will update for new ones.
729
-
730
- > Note: In order to scan for property access, this effect will run at-least once, immediately.
731
-
732
- <br />
733
- <h2 id="concept-push-event">Pushing your own events</h2>
734
-
735
- #### `state.update(key)`
736
-
737
- Fires a synthetic event, sent to all listeners for key(s) selected, be them components or callback-listeners above. Any `string` or `symbol` may be a key. Naturally, a key which is defined on state already, will be treated as an update.
738
-
739
- > Events make it easier to design around closures and callbacks, keeping as few things on your model as possible. Event methods may also be used externally, for other code to interact with.
740
-
741
- <br />
742
- <h2 id="concept-builtin-event">Listening for built-in events</h2>
743
-
744
- Controllers will emit lifecycle events for a bound component (depending on the hook).
745
-
746
- > Often lifecycle is critical to a controller's correct behavior. While we do have lifecycle-methods, it's recommended to use events where able. This way, if your class is extended and redefines a handler, yours is not at the mercy of a `super[event]()` call.
747
-
748
- Lifecycle events share names with their respective methods, [listed here](#lifecycle-api).
749
-
750
- <br />
751
-
752
- ## Event handling in-practice:
753
-
754
- ```js
755
- class TickTockClock extends Model {
756
- constructor(){
757
- super();
758
-
759
- // begin timer after component mounts
760
- this.once("componentDidMount", this.start);
761
- }
762
-
763
- seconds = 0;
764
-
765
- get minutes(){
766
- return Math.floor(this.seconds / 60);
767
- }
768
-
769
- tickTock(seconds){
770
- if(seconds % 2 == 1)
771
- console.log("tick")
772
- else
773
- console.log("tock")
774
- }
775
-
776
- logMinutes(minutes){
777
- if(minutes)
778
- console.log(`${minutes} minutes have gone by!`)
779
- }
780
-
781
- start(){
782
- const self = setInterval(() => this.seconds++, 1000);
783
- const stop = () => clearInterval(self);
784
-
785
- // run callback every time 'seconds' changes
786
- this.on("seconds", this.tickTock);
787
-
788
- // run callback when 'minutes' returns a new value
789
- this.on("minutes", this.logMinutes);
790
-
791
- // run callback when component unmounts
792
- this.once("componentWillUnmount", stop);
793
- }
794
- }
795
- ```
796
-
797
- > We saved not only two methods, but kept interval in scope, rather than a property. Pretty clean!
798
-
799
- <br />
800
- <h2 id="concept-external">Observing Props</h2>
801
-
802
- Sometimes, we want to react to changes coming from outside, usually via props. Observing a value requires that you integrate it, however after that, consumption is easy.
803
-
804
- <h4 id="method-using"><code>Model.using(source, keys?)</code></h4>
805
-
806
- > Roughly equivalent to [`uses()`](#concept-passing-props)
807
-
808
- This method helps watch an object by running [`assign`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) on it *every render*. Because controllers only react to new values, this makes for a simple way to watch externals. Combine this with getters and event-listeners, to do things when inputs change.
809
-
810
- ```ts
811
- class ActivityTracker extends Model {
812
- active = undefined;
813
-
814
- get status(){
815
- return this.active ? "active" : "inactive";
816
- }
817
-
818
- constructor(){
819
- super();
820
-
821
- this.on("active", (yes) => {
822
- if(yes)
823
- alert("Tracker just became active!")
824
- })
825
- }
826
- }
827
- ```
828
- ```jsx
829
- const DetectActivity = (props) => {
830
- const { status } = ActivityTracker.using(props);
831
-
832
- return (
833
- <big>
834
- This element is currently {status}.
835
- </big>
836
- );
837
- }
838
- ```
839
-
840
- ```jsx
841
- const Activate = () => {
842
- const [active, setActive] = useState(false);
843
-
844
- return (
845
- <div onClick={() => setActive(!active)}>
846
- <DetectActivity active={active} />
847
- </div>
848
- )
849
- }
850
- ```
851
-
852
- <sup><a href="https://codesandbox.io/s/example-using-method-wvrtu">View in CodeSandbox</a></sup>
853
-
854
- > **Note:** Method is also picky (ala `uses`), and will ignore values which don't already exist in state.
855
-
856
- <br />
857
- <h2 id="concept-async">Working with async and callbacks</h2>
858
-
859
- Because dispatch is taken care of, we can focus on just editing values as we need to. This makes the async stuff like timeouts, promises, callbacks, and even [Ajax](https://www.w3schools.com/xml/ajax_xmlhttprequest_send.asp) a piece of cake.
860
-
861
- ```ts
862
- class StickySituation extends Model {
863
- agent = "Bond";
864
- remaining = 60;
865
-
866
- constructor(){
867
- super();
868
- this.once("componentWillMount", this.missionStart);
869
- }
870
-
871
- missionStart(){
872
- const timer = setInterval(this.tickTock, 1000);
873
-
874
- // select may be any string, symbol, or collection of them
875
- this.once(["componentWillUnmount", "oh no!"], () => {
876
- clearInterval(timer);
877
- });
878
- }
879
-
880
- tickTock = () => {
881
- const timeLeft = this.remaining -= 1;
882
-
883
- if(timeLeft === 0)
884
- this.update("oh no!");
885
- }
886
-
887
- getSomebodyElse = async () => {
888
- const res = await fetch("https://randomuser.me/api/");
889
- const data = await res.json();
890
- const recruit = data.results[0];
891
-
892
- this.agent = recruit.name.last;
893
- }
894
- }
895
- ```
896
- ```jsx
897
- const ActionSequence = () => {
898
- const {
899
- agent,
900
- getSomebodyElse,
901
- remaining
902
- } = StickySituation.use();
903
-
904
- if(remaining === 0)
905
- return <h1>🙀💥</h1>
906
-
907
- return (
908
- <div>
909
- <div>
910
- <b>Agent {agent}</b>, we need you to diffuse the bomb!
911
- </div>
912
- <div>
913
- If you can't do it in {remaining} seconds,
914
- Schrödinger's cat may or may not die!
915
- </div>
916
- <div>
917
- But there's still time!
918
- <u onClick={getSomebodyElse}>Tap another agent</u>
919
- if you think they can do it.
920
- </div>
921
- </div>
922
- )
923
- }
924
- ```
925
- <sup><a href="https://codesandbox.io/s/async-example-inmk4">View in CodeSandbox</a></sup>
926
-
927
- > Notice how our components remain completely independent from the logic.
928
- >
929
- > If we want to modify or even duplicate our `ActionSequence`, with a new aesthetic or different verbiage, we don't need to copy or edit any of these behaviors!
930
-
931
- <br/>
932
-
933
- ### 👾 Level 2 Clear!
934
-
935
- > Nice, now we're able to create full-blown experiences, while keeping the number of hooks used to a minimum (namely one). That's good, because being able to separate this stuff promotes better patterns. Reduce, reuse, recycle.
936
- >
937
- > However we can keep going, because with Models, _sharing_ logic means more than just reusing logic.
938
-
939
- <br/>
940
- <h1 id="sharing-section">Sharing state</h1>
941
-
942
- One of the most important features of a Model is the ability to share a single, active state with any number of users, be them components or even [other controllers](#concept-ambient). Whether you need state in-context or to be usable app-wide, you can, with a number of simple abstractions.
943
-
944
- <br/>
945
-
946
- <h2 id="concept-sharing">The Basics</h2>
947
-
948
- Before going in depth, a quick example will help get the basic point across.
949
-
950
- ### Step 1
951
- Start with a normal, run-of-the-mill Model.
952
-
953
- ```js
954
- export class FooBar extends Model {
955
- foo = 0;
956
- bar = 0;
957
- };
958
- ```
959
-
960
- ### Step 2
961
- Import the creatively-named `Provider` component, and pass an instance of state to its `of` prop.
962
-
963
- ```jsx
964
- import { Provider } from "@expressive/mvc";
965
-
966
- const Example = () => {
967
- const foobar = FooBar.use();
968
-
969
- return (
970
- <Provider of={foobar}>
971
- <Foo />
972
- <Bar />
973
- </Provider>
974
- )
975
- }
976
- ```
977
- ### Step 3
978
- Where `FooBar.use()` creates an instance of `FooBar`, `FooBar.get()` fetches an *existing* instance of `FooBar`.
979
-
980
- ```jsx
981
- const Foo = () => {
982
- const { foo } = FooBar.get();
983
-
984
- return <p>The value of foo is {foo}!</p>
985
- }
986
-
987
- const Bar = () => {
988
- const { bar } = FooBar.get();
989
-
990
- return <p>The value of bar is {bar}!</p>
991
- }
992
- ```
993
-
994
- Using the class itself, we have a convenient way to "select" what type of state we want, assuming it's in context.
995
-
996
- > There's another big benefit here: *types are preserved.*
997
- >
998
- > Models, when properly documented, maintain autocomplete, hover docs, and intellisense, even as you pass controllers all-throughout your app! 🎉
999
-
1000
- <br/>
1001
-
1002
- <h2 id="concept-context">Providing via Context</h2>
1003
-
1004
- Let's first create and cast a state, for use by its components and peers. In the [next chapter](#access-section), we'll expand on how to consume them.
1005
-
1006
- By default, `Model` uses [React Context](https://frontarm.com/james-k-nelson/usecontext-react-hook/) to find instances from upstream.
1007
-
1008
- There are several ways to provide a controller. Nothing special on the model is required.
1009
-
1010
- ```ts
1011
- export class FooBar extends Model {
1012
- foo = 0;
1013
- bar = 0;
1014
- };
1015
- ```
1016
-
1017
- > Note the `export` here.
1018
- >
1019
- > Models and their consumers do not need to live in same file, let alone module. Want to interact with another library's state? This can make for a pro-consumer solution. ☝️😑
1020
-
1021
- <br/>
1022
- <h2 id="concept-provider">Providing an instance</h2>
1023
-
1024
- Here, pass a controller to the `of` prop. For any children, it will be made accessible via its class.
1025
-
1026
- > Unlike a normal `Context.Provider`, a `Provider` is generic and good for any (or many) different states.
1027
-
1028
- ```jsx
1029
- import { Provider } from "@expressive/mvc";
1030
- import { FooBar } from "./controller";
1031
-
1032
- export const App = () => {
1033
- const foobar = FooBar.use();
1034
-
1035
- return (
1036
- <Provider of={foobar}>
1037
- <Foo/>
1038
- <Bar/>
1039
- </Provider>
1040
- )
1041
- }
1042
- ```
1043
-
1044
- <br/>
1045
- <h2 id="concept-provider-spawning">Spawning an instance</h2>
1046
-
1047
- Without constructor-arguments, creating the instance separately can be an unnecessary step. Pass a Model itself, to both create and provide a state in one sitting.
1048
- ```jsx
1049
- export const App = () => {
1050
- return (
1051
- <Provider of={FooBar}>
1052
- <Foo/>
1053
- <Bar/>
1054
- </Provider>
1055
- )
1056
- }
1057
- ```
1058
-
1059
- <br/>
1060
- <h2 id="concept-provider-props">Spawning with props</h2>
1061
-
1062
- When a Model is passed to `of` directly, the Provider will behave similarly to [`Model.using()`](#concept-external). All other props are forwarded to state and likewise observed.
1063
-
1064
- ```jsx
1065
- const FancyFooBar = (props) => {
1066
- return (
1067
- <Provider of={FooBar} foo={props.foo} bar={10}>
1068
- {props.children}
1069
- </Provider>
1070
- )
1071
- }
1072
- ```
1073
-
1074
- > Now, consumers can see incoming-data, which only the parent has access to!
1075
-
1076
- <br/>
1077
- <h2 id="concept-provider-multi">Providing Multiple</h2>
1078
-
1079
- Finally, `of` can accept a collection of models and/or state objects. Mix and match as needed to ensure a DRY, readible root.
1080
-
1081
- ```jsx
1082
- const MockFooBar = () => {
1083
- const hello = Hello.use("world");
1084
-
1085
- return (
1086
- <Provider of={{ hello, Foo, Bar }}>
1087
- {props.children}
1088
- </Provider>
1089
- )
1090
- }
1091
- ```
1092
-
1093
- <br/>
1094
-
1095
- <h1 id="section-global">Global Models</h1>
1096
-
1097
- While Providers are nice for contextual-isolation, sometimes we want just one controller for a purpose in an entire app. Think of concepts like login, routes, or interaction with an API.
1098
-
1099
- > For instance, if ever used [react-router](https://github.com/ReactTraining/react-router), you'll know `<BrowserRouter>` is only needed for its Provider. You never have more than one at a time, so context is somewhat moot.
1100
-
1101
- For this reason we have `Singleton`, a type of state to exist everywhere. Hooks will present the same as their `Model` counterparts, but under the hood, always retrieve a single, promoted instance.
1102
-
1103
- <br/>
1104
-
1105
- ## Defining a Singleton
1106
-
1107
- Simply extend `Singleton`, itself a type of `Model`.
1108
-
1109
- ```js
1110
- import { Singleton } from "@expressive/mvc";
1111
- import { getCookies } from "./monster";
1112
-
1113
- class Login extends Singleton {
1114
- loggedIn = false;
1115
- userName = undefined;
1116
-
1117
- componentDidMount(){
1118
- this.resumeSession();
1119
- }
1120
-
1121
- async resumeSession(){
1122
- const { user } = await getCookies();
1123
-
1124
- if(user){
1125
- loggedIn = true;
1126
- userName = user.name;
1127
- }
1128
- }
1129
- }
1130
- ```
1131
- > Assume this class for examples below.
1132
-
1133
- <br/>
1134
-
1135
- <h2 id="#section-singleton-activate">Activating a Singleton</h2>
1136
-
1137
- Singletons will not be useable, until they're initialized in one of three ways:
1138
-
1139
-
1140
- ### Method 1:
1141
- **`Singleton.create(...)`**
1142
-
1143
- A method on all Models, `create` on a Singleton will also promote that new instance. This can be done anytime, as long as it's before a dependant (component or peer) tries to pull data from it.
1144
-
1145
- ```js
1146
- window.addEventListener("load", () => {
1147
- const loginController = Login.create();
1148
-
1149
- loginController.resumeSession();
1150
-
1151
- ReactDOM.render(<App />, document.getElementById("root"));
1152
- });
1153
- ```
1154
-
1155
- ### Method 2:
1156
- **`Singleton.use()`**
1157
-
1158
- Create an instance with the standard use-methods.
1159
-
1160
- ```jsx
1161
- const LoginGate = () => {
1162
- const { get: login, loggedIn, userName } = Login.use();
1163
-
1164
- return loggedIn
1165
- ? <Welcome name={userName} />
1166
- : <Prompt onClick={() => login.resumeSession()} />
1167
- }
1168
- ```
1169
- An instance of `Login` is available immediately after `use()` returns.
1170
- However, it will also become unavailable if `LoginPrompt` does unmount. If it mounts again, any _new_ subscribers get the latest version.
1171
-
1172
- ### Method 3:
1173
- **`<Provider of={Singleton}>`**
1174
-
1175
- ```jsx
1176
- export const App = () => {
1177
- return (
1178
- <>
1179
- <Provider of={Login} />
1180
- <UserInterface />
1181
- </>
1182
- )
1183
- }
1184
- ```
1185
- This will have no bearing on context, it will simply be "provided" to everyone. Wrapping children, in this case, is doable but optional. The active instance is likewise destroyed when its Provider unmounts.
1186
-
1187
- > You will most likely use this to limit the existence of a Singleton (and its side-effects), to when a particular UI is on-screen.
1188
-
1189
- <br/>
1190
-
1191
- <h1 id="access-section">Consuming shared state</h1>
1192
-
1193
- Whether a state extends `Model` or `Singleton` won't matter; they both present the same.<br/>
1194
-
1195
- ```ts
1196
- export class FooBar extends Model {
1197
- foo = 0;
1198
- bar = 0;
1199
- };
1200
- ```
1201
-
1202
- <br/>
1203
- <h2 id="concept-consumer-hooks">Method Hooks</h2>
1204
-
1205
- Models make fetching a state super easy. On every model, there are defined three `useContext` like methods. They find, return and maintain the nearest instance of class they belong to.
1206
-
1207
- <h4 id="method-get"><code>.get(key?)</code></h4>
1208
-
1209
- The most straight-forward, finds the nearest `instanceof this` within context. If given a key it will 'drill', either returning a value (if found) or `undefined`. Will not respond to updates.
1210
-
1211
- <h4 id="method-tap"><code>.tap(key?, expect?)</code></h4>
1212
-
1213
- Will subscribe to values on fetched instance, much like `use` will.
1214
- > Children sharing a mutual state, thusly can affect _eachother's_ state.
1215
-
1216
- ```jsx
1217
- const InnerFoo = () => {
1218
- const { bar, set } = FooBar.tap();
1219
-
1220
- return (
1221
- <>
1222
- <pre onClick={() => set.foo++}>Foo</pre>
1223
- <small>(Bar was clicked {bar} times!)</small>
1224
- </>
1225
- )
1226
- }
1227
- ```
1228
- ```jsx
1229
- const InnerBar = () => {
1230
- const { foo, set } = FooBar.tap();
1231
-
1232
- return (
1233
- <>
1234
- <pre onClick={() => set.bar++}>Bar</pre>
1235
- <small>(Foo was clicked {foo} times!)</small>
1236
- </>
1237
- )
1238
- }
1239
- ```
1240
- <sup><a href="https://codesandbox.io/s/provider-example-5vvtr">View in CodeSandbox</a></sup>
1241
-
1242
- With the `expect` flag, tap will throw if chosen property is undefined at the time of use. This is useful because output is cast as non-nullable. This way, values may be destructured without assertions (or complaints from your linter).
1243
-
1244
- <h4 id="method-tap"><code>.tag(id)</code> / <code>.tag(idFactory)</code></h4>
1245
-
1246
- Covered in a [later section](#concept-lifecycle), `tag` will also report lifecycle-events to its parent controller. An `id` argument is used to setup and identify the source of such updates.
1247
-
1248
- <br/>
1249
-
1250
- <h2 id="concept-consumer">Consumers</h2>
1251
-
1252
- Hooks are great, but not all components are functions. For those we have the `Consumer` component. This is also generic, and able to capture state for any Model passed to its `of` prop.
1253
-
1254
- There are a number of options, to get info with or without a typical subscription.
1255
-
1256
- <h3 id="consumer-render">Render Function</h3>
1257
-
1258
- When a function is passed as a child, returned elements will replace the Consumer. The function recieves its state as first argument, and subscribed updates will trigger new renders.
1259
-
1260
- > Note: If you spread in `props` directly, _all_ possible values will be subscribed to.
1261
-
1262
- ```jsx
1263
- const CustomConsumer = () => {
1264
- return (
1265
- <Consumer of={ModernController}>
1266
- ({ value }) => <OldSchoolComponent value={value} />
1267
- </Consumer>
1268
- )
1269
- }
1270
- ```
1271
-
1272
- Consumers allow state to drive classical components easily, assuming the values on that state match up with expected props.
1273
-
1274
- <h3 id="consumer-tap"><code>tap</code> Prop</h3>
1275
-
1276
- If rendered output isn't needed, you can define a handler on the `tap` prop. This is better for side-effects, more specific to a UI than a controller itself.
1277
- > Also allows for `async`, since return value (a promise) can be ignored.
1278
-
1279
- ```jsx
1280
- const ActiveConsumer = () => {
1281
- return (
1282
- <Consumer of={Timer} tap={({ minutes, seconds }) => {
1283
- console.log(`Timer has been running for ${minutes}m ${seconds}s!`)
1284
- }}/>
1285
- )
1286
- }
1287
- ```
1288
-
1289
- <h3 id="consumer-get"><code>get</code> Prop</h3>
1290
-
1291
- Callback runs per-render of the parent component, rather than per-update.
1292
-
1293
- ```jsx
1294
- const LazyConsumer = () => {
1295
- return (
1296
- <Consumer of={Timer} get={({ minutes, seconds }) => {
1297
- console.log(`Timer is currently at ${minutes}m ${seconds}s!`)
1298
- }}/>
1299
- )
1300
- }
1301
- ```
1302
-
1303
- > Use `<Consumer />` to intregrate traditional components, almost as easily as modern function-components!
1304
-
1305
- <br />
1306
- <h2 id="concept-ambient">Peer Consumers</h2>
1307
-
1308
- While context is great for components, controllers can use it too, that of the component where used. This is helped by importing a special function, likewise called `tap`.
1309
- > [This is an instruction](#concept-instruction), in-short a factory function. It tells the controller while initializing: the defined property is special, run some logic to set that up.
1310
-
1311
- On your Model, assign a property with the value of `tap(Model)`, passing in the type of state you're interested in.
1312
-
1313
- As a new controller spawns, it will try and fetch the requested instance. If found, the peer will be accessible to methods, accessors, etc. via that property.
1314
-
1315
- #### `tap(Type, required?)`
1316
-
1317
- ```js
1318
- import Model, { tap } from "@expressive/mvc";
1319
-
1320
- class Hello extends Model {
1321
- to = "World";
1322
- }
1323
-
1324
- class Greet extends Model {
1325
- hello = tap(Hello);
1326
-
1327
- get greeting(){
1328
- return `Hello ${this.hello.to}`;
1329
- }
1330
- }
1331
- ```
1332
- Create a controller which taps another one. Make sure it's wrapped by proper `Provider`, and the controller will fetch that instance from context.
1333
- ```js
1334
- import { Provider } from "@expressive/mvc";
1335
-
1336
- const App = () => {
1337
- return (
1338
- <Provider of={Hello}>
1339
- <SayHi />
1340
- </Provider>
1341
- )
1342
- }
1343
-
1344
- const SayHi = () => {
1345
- const { greeting } = Greet.tap()
1346
-
1347
- return (
1348
- <p>{greeting}!</p>
1349
- )
1350
- }
1351
- ```
1352
- Here *SayHi*, without direct access to the *Hello* controller, can still get what it needs. The actionable value `greeting` comes from *Greet*, which itself gets data from *Hello*. This is an example of scope-control, allowing code to written as needs-based. Generally speaking, this often makes a program more robust and maintainable.
1353
-
1354
- <br/>
1355
-
1356
- ### 🎮 Level 3 Complete!
1357
-
1358
- > Here we've learned how to make state _accessible_, in more ways than one. Now with these tools we're able to tackle more advanced connectivity. Next we'll get into _ecosystems_ and how to split behavior into simple, often-reusable chunks and hook <sup>(pun intended)</sup> them together.
1359
-
1360
- <br/>
1361
- <h1 id="composing-section">Composing Models</h1>
1362
-
1363
- Ultimately the purpose of Models, and be that classes in-general, is to compartmentalize logic. This makes code clear and easy to duplicate. Ideally, we want controllers to be really good at one thing, and to cooperate with other systems as complexity grows.
1364
-
1365
- > **This** is how we'll build better performing, easier to work on applications.
1366
-
1367
- In broad strokes, here are some of the ways to set mutliple states up, to work together:
1368
- <br/><br/>
1369
-
1370
- <h2 id="concept-compose">Simple Composition</h2>
1371
-
1372
- Nothing prevents the use of more than one state per component. Take advantage of this to combine smaller states, rather than make big, monolithic state.
1373
-
1374
- ```js
1375
- class Foo extends Model {
1376
- value = 1
1377
- }
1378
-
1379
- class Bar extends Model {
1380
- value = 2
1381
- }
1382
-
1383
- const FibonacciApp = () => {
1384
- const foo = Foo.use();
1385
- const bar = Bar.use();
1386
-
1387
- return (
1388
- <div>
1389
- <div onClick={() => { foo.value += bar.value }}>
1390
- Foo's value is {foo.value}, click to add bar!
1391
- </div>
1392
- <div onClick={() => { bar.value += foo.value }}>
1393
- Bar's value is {bar.value}, click to add foo!
1394
- </div>
1395
- </div>
1396
- )
1397
- }
1398
- ```
1399
- <sup><a href="https://codesandbox.io/s/example-simple-compose-dew5p">View in CodeSandbox</a></sup>
1400
-
1401
- <br/>
1402
- <h2 id="concept-child-model">Child Controllers</h2>
1403
-
1404
- While the above works fine, what if we want something more organized and reusable? Fortunately, we can "wrap" these models into another, and use that one instead. Think of it like building a custom hook out of smaller (or the core React) hooks.
1405
-
1406
- #### `use(Type, callback?)`
1407
-
1408
- Import another instruction, `use` and pass it a Model to attach a child-instance upon creation.
1409
-
1410
- ```js
1411
- import Model, { use } from "@expressive/mvc";
1412
-
1413
- class FooBar extends Model {
1414
- foo = use(Foo);
1415
- bar = use(Bar);
1416
-
1417
- get value(){
1418
- return this.foo.value + this.bar.value;
1419
- }
1420
-
1421
- addFoo = () => this.bar.value = this.value;
1422
- addBar = () => this.foo.value = this.value;
1423
- }
1424
- ```
1425
-
1426
- > Now, we have a good way to mixin controllers. More importantly though, we have a place to put _mutually-inclusive_ things, before which had to live with JSX. This brings us back to dumb-component paradise. 🏝
1427
-
1428
- ```js
1429
- const FibonacciApp = () => {
1430
- const { foo, bar, value, addFoo, addBar } = FooBar.use();
1431
-
1432
- return (
1433
- <div>
1434
- <div>
1435
- Foo + Bar = {value}
1436
- </div>
1437
- <div onClick={addBar}>
1438
- Foo's value is ${foo.value}, click to add in bar!
1439
- </div>
1440
- <div onClick={addFoo}>
1441
- Bar's value is ${bar.value}, click to add in foo!
1442
- </div>
1443
- </div>
1444
- )
1445
- }
1446
- ```
1447
- <sup><a href="https://codesandbox.io/s/example-child-controllers-xx4c9">View in CodeSandbox</a></sup>
1448
-
1449
- > Subscriber will not only subscribe to `FooBar` updates, but that of its children as well! This lets us keep complexity out of components, and add features without adding techincal debt.
1450
-
1451
- <br/>
1452
- <h2 id="concept-parent-model">Parent Controller</h2>
1453
-
1454
- While separating concerns, there will be times where designing a Model expressly to help another makes sense, and a reference to that parent is desirable. For this we have the `parent` instruction.
1455
-
1456
- #### `parent(Type, required?)`
1457
-
1458
- Returns a reference to the controller which `this` is a child. Optional parameter, `required` will throw if child model is created outside of that parent, otherwise property may be `undefined`.
1459
-
1460
- ```js
1461
- import Model, { use, parent } from "@expressive/mvc";
1462
-
1463
- class Control extends Model {
1464
- child = use(Dependant);
1465
- }
1466
-
1467
- class Dependant extends Model {
1468
- parent = parent(Control, true);
1469
- }
1470
- ```
1471
-
1472
- <br/>
1473
-
1474
- ### 🧱 Level 4 Complete!
1475
-
1476
- > Here we've learned how Models allow controllers to cooperate and play off of eachother. They allow us to break-up behavior into smaller pieces, building-blocks if you will. With that, we have an easy way to separate concerns, freeing up focus for more added nuance and features.
1477
-
1478
- <br/>
1479
-
1480
- ### :construction: More Docs are on the way! 🏗
1481
-
1482
- <br/>
1483
- <h1>Best Practices</h1>
1484
- Good things to keep in mind of as you build with MVC patters in your apps. Naturally, just suggestions, but these are design principles to be supported as this library evolves.
1485
-
1486
- <br/>
1487
- <h2 id="concept-typescript">Models & Typescript</h2>
1488
-
1489
- Typescript is your friend and Expressive is built laregely to facilitate its use within React apps.
1490
-
1491
- A ton of focus has been put in to make sure _all_ features of Expressive are in-line with what Typescript lanaguage supports. Transparency is key for a great developer experience, and MVC's are designed to infer often and fail-fast in static analysis.
1492
-
1493
- ```ts
1494
- import Model from "@expressive/mvc";
1495
-
1496
- class FunActivity extends Model {
1497
- /** Interval identifier for cleaning up */
1498
- interval: number;
1499
-
1500
- /** Number of seconds that have passed */
1501
- secondsSofar: number;
1502
-
1503
- constructor(alreadyMinutes: number = 0){
1504
- super();
1505
-
1506
- this.secondsSofar = alreadyMinutes * 60;
1507
- this.interval = setInterval(() => this.secondsSofar++, 1000)
1508
- }
1509
-
1510
- /** JSDocs too can help provide description beyond simple
1511
- * autocomplete, making it easier reduce, reuse and repurpose. */
1512
- willUnmount(){
1513
- clearInterval(this.interval)
1514
- }
1515
- }
1516
- ```
1517
- ```jsx
1518
- const PaintDrying = ({ alreadyMinutes }) => {
1519
- /* Your IDE will know `alreadyMinutes` is supposed to be a number */
1520
- const { secondsSofar } = FunActivity.use(alreadyMinutes);
1521
-
1522
- return (
1523
- <div>
1524
- I've been staring for like, { secondsSofar } seconds now,
1525
- and I'm starting to see what this is all about! 👀
1526
- </div>
1527
- )
1528
- }
1529
- ```
1530
-
1531
- <br/>
1532
-
1533
- <h1>Concepts</h1>
1534
- Topics to help in understanding how MVC's work under the hood.
1535
-
1536
- <br/>
1537
- <br/>
1538
- <h2 id="concept-lazy">Subscription based "lazy" updating</h2>
1539
-
1540
- Controllers use a subscription model to decide when to render, and will **only** refresh for values which are actually used. They do this by watching property access *on the first render*, within a component they hook up to.
1541
-
1542
- That said, while hooks can't actually read your function-component, destructuring is a good way to get consistent behavior. Where a property *is not* accessed on initial render render (inside a conditional or ternary), it could fail to update as expected.
1543
-
1544
- Destructuring pulls out properties no matter what, and so prevents this problem. You'll also find also reads a lot better, and promotes good habits.
1545
-
1546
- <br/>
1547
-
1548
- ```jsx
1549
- class FooBar {
1550
- foo = "bar"
1551
- bar = "foo"
1552
- }
1553
-
1554
- const LazyComponent = () => {
1555
- const { set, foo } = use(FooBar);
1556
-
1557
- return (
1558
- <h1
1559
- onClick={() => set.bar = "baz" }>
1560
- Foo is {foo} but click here to update bar!
1561
- </h1>
1562
- )
1563
- }
1564
- ```
1565
-
1566
- > Here `LazyComponent` will not update when `bar` does change, because it only accessed `foo` here.
1567
-
1568
- <br/>
1569
- <h2 id="concept-debounce">Automatic debounce</h2>
1570
-
1571
- Changes made synchronously are batched as a single new render.
1572
-
1573
- ```jsx
1574
- class ZeroStakesGame extends Model {
1575
- foo = "bar"
1576
- bar = "baz"
1577
- baz = "foo"
1578
-
1579
- shuffle = () => {
1580
- this.foo = "???"
1581
- this.bar = "foo"
1582
- this.baz = "bar"
1583
-
1584
- setTimeout(() => {
1585
- this.foo = "baz"
1586
- }, 1000)
1587
- }
1588
- }
1589
- ```
1590
- ```jsx
1591
- const MusicalChairs = () => {
1592
- const { foo, bar, baz, shuffle } = ZeroStakesGame.use();
1593
-
1594
- return (
1595
- <div>
1596
- <span>Foo is {foo}'s chair!</span>
1597
- <span>Bar is {bar}'s chair!</span>
1598
- <span>Baz is {baz}'s chair!</span>
1599
-
1600
- <div onClick={shuffle}>🎶🥁🎶🎷🎶</div>
1601
- </div>
1602
- )
1603
- }
1604
- ```
1605
- <!-- <sup><a href="https://codesandbox.io/s/example-debouncing-sn1mq">View in CodeSandbox</a></sup> -->
1606
-
1607
- > Even though we're ultimately making four updates, `use()` only needs to re-render twice. It does so once for everybody (being on the same [event-loop](https://blog.sessionstack.com/how-javascript-works-event-loop-and-the-rise-of-async-programming-5-ways-to-better-coding-with-2f077c4438b5)), resets when finished, and again wakes for `foo` when it decides settle in.
1608
-
1609
-
1610
- <!-- <br/>
1611
- <h2 id="concept-debounce">Selectors</h2>
1612
- -->
1613
-
1614
- <br/>
1615
- <br/>
1616
-
1617
- <h1>API</h1>
1618
-
1619
- <h2 id="controller-api">Model</h2>
1620
-
1621
- Set behavior for certain properties on classes extending `Model`.
1622
-
1623
- While standard practice is for `use` to take all methods (and bind them), all properties (and watch them), there are special circumstances to be aware of. <br /><br />
1624
-
1625
- <h2 id="singleton-api">Singleton</h2>
1626
-
1627
- Set behavior for certain properties on classes extending `Controller`.
1628
-
1629
- While standard practice is for `use` to take all methods (and bind them), all properties (and watch them), there are special circumstances to be aware of. <br /><br />
1630
-
1631
- <h2 id="reserved-api">Reserved</h2>
1632
-
1633
- #### `set` / `get`
1634
- - Not to be confused with setters / getters, a circular reference to `state`
1635
- - this is useful to access your state object while destructuring
1636
-
1637
- #### `export<T>(this: T, keys?: string[]): { [P in keyof T]: T[P] }`
1638
- - return a snapshot of live state
1639
-
1640
- <br />
1641
-
1642
- <h2 id="lifecycle-api">Lifecycle</h2>
1643
-
1644
- > Lifecycle methods are called syncronously, via `useLayoutEffect` and thus may block or intercept a render. If you prefer to run side-effects rather, use corresponding events or define your method as `async`.
1645
-
1646
- #### `componentDidMount`
1647
- - `use()` will call this while internally mounting for the first time.
1648
-
1649
- #### `componentWillUnmount`
1650
- - `use()` will call this before starting to clean up.
1651
-
1652
- #### `componentWillRender(): void`
1653
- - Called every render.
1654
-
1655
- #### `componentWillMount(): void`
1656
- - Called on first render, prior to component being drawn on-screen.
1657
-
1658
- #### `componentWillUpdate(): void`
1659
- - Called every subsequent render of the same component.
1660
-
1661
- <br/>
1662
- <br/>
1663
-
1664
- # License
1665
-
1666
- MIT license.