sygnal 4.0.2 → 4.1.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 CHANGED
@@ -2,8 +2,7 @@
2
2
 
3
3
  An intuitive framework for building fast, small and composable components or applications.
4
4
 
5
- Sygnal is built on top of Cycle.js, and allows you to write functional reactive, Observable based, components with fully isolated side-effects without
6
- having to worry about the complex plumbing usually associated with functional reactive programming.
5
+ Sygnal is built on top of [Cycle.js](https://cycle.js.org/), and allows you to write functional reactive, Observable based, components with fully isolated side-effects without having to worry about the complex plumbing usually associated with functional reactive programming.
7
6
 
8
7
  Components and applications written using Sygnal look similar to React functional components, and can be nested just as easily, but have many benefits including:
9
8
  - 100% pure components with absolutely no side effects
@@ -13,805 +12,206 @@ Components and applications written using Sygnal look similar to React functiona
13
12
  - Fast rendering
14
13
  - Close to zero boiler plate code
15
14
 
16
- ## Built on Cycle.js
17
-
18
- Sygnal is built on top of Cycle.js which is a functional reactive coding framework that asks 'what if the user was a function?'
19
-
20
- It is worth reading the summary on the [Cycle.js homepage](https://cycle.js.org/ "Cycle.js Homepage"), but essentially Cycle.js allows you to write simple, concise, extensible, and testable code using a functional reactive style, and helps ensure that all side-effects are isolated away from your component code.
21
-
22
- Sygnal takes it a step further, and makes it easy to write arbitrarily complex applications that have all of the Cycle.js benefits, but with a much easier learning curve, and virtually no complex plumbing or boiler plate code.
23
-
24
- ## Goals of Sygnal
25
-
26
- Sygnal provides a structured way to create components that accomplishes several key goals:
27
- - Minimize boilerplate
28
- - Provide a simplified way to handle common application tasks without a bloated API
29
- - Handle all stream plumbing between components automatically
30
- - Support arbitrarily complex applications with deep component hierarchies
31
- - Reuse the best patterns from popular frameworks like React and Vue while avoiding the pitfalls
32
- - Support pure Javascript, Typescript (in progress), and JSX
33
- - Provide state out of the box, and make it easy to use
34
- - Provide meaningful debugging information
35
- - Work with modern bundlers like Vite, and provide easy application bootstrapping
36
-
37
-
38
- ## In a Nutshell...
15
+ ## Documentation
39
16
 
40
- Sygnal is easiest to understand by example, so let's walk through some now.
17
+ - **[Getting Started](./docs/getting-started.md)** Installation, setup, and your first interactive component
18
+ - **[Guide](./docs/guide.md)** — In-depth coverage of all features: state, MVI, collections, drivers, context, TypeScript, and more
19
+ - **[API Reference](./docs/api-reference.md)** — Complete reference for all exports, types, and configuration options
41
20
 
42
-
43
- ### Installation
44
-
45
- To install Sygnal, just use your favorite package manager to grab the `sygnal` package
21
+ ## Quick Start
46
22
 
47
23
  ```bash
48
- npm install sygnal
49
- ```
50
-
51
- > NOTE:
52
- > To use JSX like in the examples below, you'll need to use a bundler like Vite or Rollup, and configure it to use Sygnal's JSX functions. For Vite, add the following to your vite.config.js file:
53
- > ```javascript
54
- > esbuild: {
55
- > jsxFactory: `jsx`,
56
- > jsxInject: `import { jsx } from 'sygnal/jsx'`
57
- > },
58
- > ```
59
- > You can use Sygnal without JSX, but will need to replace the HTML with vDom functions:
60
- > `<div>Hello</div>` becomes `h('div', 'Hello')`
61
- >
62
- > Import `h()` from the sygnal package with `import { h } from 'sygnal'`
63
-
64
- ### React Style Functional Components
65
-
66
- If you're coming from React, Sygnal components will feel familiar. Just as with React Functional Components, Sygnal components begin with a function which defines the component's view (what HTML elements are rendered).
67
-
68
- The simplest Sygnal component is just a function that returns JSX or vDom:
69
-
70
- ```javascript
71
- // Hello World Component
72
- function MyComponent() {
73
- return (
74
- <div>Hello World</div>
75
- )
76
- }
77
- ```
78
-
79
- ### View Parameters
80
-
81
- Also similar to React, the main function of a component will receive any props and children set on it. But in Sygnal, you also get additional values for the state, context, and any Peer component vDom (we will cover the last two items a bit later).
82
-
83
- > In a Sygnal application, props can still play an important role, but the state is where most of the magic happens. In general you should think of props as how to configure a component, and use state for everything else.
84
-
85
- To access any of these values, simply use destructuring in the function arguments:
86
-
87
- ```javascript
88
- // getting the state, props, and children in a Sygnal component's main function
89
- // (there are also 'context' and named items for Peer components, but we'll cover those later)
90
- function MyComponent({ state, props, children }) {
91
- return (
92
- <div className={ props.className }>
93
- <h1>{ state.title }</h1>
94
- { children }
95
- </div>
96
- )
97
- }
98
-
99
- // ...and using it in another component
100
- function RootComponent() {
101
- return (
102
- <div>
103
- <MyComponent className="some-class">
104
- <span>Here's a child</span>
105
- <span>Here's another child</span>
106
- </MyComponent>
107
- </div>
108
- )
109
- }
110
- ```
111
-
112
-
113
- ### Root Component (Starting a Sygnal Application)
114
-
115
- Just like with React and other popular frameworks, Sygnal applications start with a root level component which is passed to Sygnal's run() function:
116
-
117
- To start an application using the components above, just import run() from Sygnal, and pass RootComponent to it:
118
-
119
- ```javascript
120
- import {run} from 'sygnal'
121
-
122
- // this will attach RootComponent to an HTML element with the id of "root"
123
- run(RootComponent)
124
-
125
- // to attach to a different element, use the mountPoint option:
126
- run(RootComponent, {}, { mountPoint: '#css-selector' })
24
+ npx degit tpresley/sygnal-template my-app
25
+ cd my-app
26
+ npm install
27
+ npm run dev
127
28
  ```
128
29
 
30
+ Or install manually:
129
31
 
130
- ### State (The Basics)
131
-
132
- Every Sygnal application is provided with state which is automatically passed to every component in the hierarchy.
133
-
134
- We'll talk about how to change state, and control the state passed to sub components in a bit, but first we need to set the initial state on the root component.
135
-
136
- The easiest way to set the initial state is to augment the main component function with an .initialState property:
137
-
138
- ```javascript
139
- function RootComponent({ state }) {
140
- return (
141
- <div>Hello { state.name }!</div>
142
- )
143
- }
144
-
145
- // this will set the initial state with a 'name' property
146
- // so the component above will render 'Hello World!'
147
- RootComponent.initialState = {
148
- name: 'World'
149
- }
32
+ ```bash
33
+ npm install sygnal
150
34
  ```
151
35
 
152
- > Notice that .initialState is added directly to the RootComponent function. In Sygnal, we use this form of function object augmentation to add all configuration to components
153
-
36
+ ## In a Nutshell
154
37
 
155
- ### Drivers
38
+ A Sygnal component is a function (the **view**) with static properties that define **when** things happen (`.intent`) and **what** happens (`.model`):
156
39
 
157
- Sygnal components should always be pure, meaning they should never produce side effects or attempt to maintain their own state. In practice, this means any time a component needs to 'do' anything, from changing the state, making a network call, or anything else, the component doesn't do it internally, but sends a signal to one or more drivers which take care of actually performing the action.
158
-
159
- But in order for components to decide what to do, they need to know what's happening around them. Drivers take care of that as well.
160
-
161
- Drivers in a Sygnal application act as both a 'source' and a 'sink'. They provide information to components through their 'source' objects, and get commands from components to do things in their 'sink'.
162
-
163
- Some drivers only provide sources, and others only accept sinks, but most are both sources *and* sinks. And for most applications, the drivers you'll be using the most are the `STATE` and `DOM` drivers, both of which are included automatically in all Sygnal applications.
164
-
165
- Here's a simple component that uses the DOM driver source, and STATE driver sink to display a count and increment it whenever the button is pressed:
166
-
167
- ```javascript
168
- // View: How the component is displayed
169
- function RootComponent({ state }) {
40
+ ```jsx
41
+ function Counter({ state }) {
170
42
  return (
171
43
  <div>
172
- <span>{ state.count }</span>
173
- <button type="button" className="increment-button" />
44
+ <h1>Count: {state.count}</h1>
45
+ <button className="increment">+</button>
46
+ <button className="decrement">-</button>
174
47
  </div>
175
48
  )
176
49
  }
177
50
 
178
- RootComponent.initialState = {
179
- count: 0
180
- }
181
-
182
- // Intent: WHEN things should happen
183
- RootComponent.intent = ({ DOM }) => {
184
- return {
185
- // this is an 'action', and is triggered whenever the button is clicked
186
- // DOM here is a driver 'source', and tells you what's happening in the browser DOM
187
- INCREMENT: DOM.select('.increment-button').events('click')
188
- }
189
- }
190
-
191
- // Model: WHAT things can/should happen, and what drivers to use to do it
192
- RootComponent.model = {
193
- // this is the same 'action' as above, and runs whenever
194
- // the action above is triggered
195
- INCREMENT: {
196
- // this is a STATE 'sink'
197
- // the function is a 'reducer' and takes the current state
198
- // and must return what the new state should be
199
- STATE: (state) => {
200
- return { count: state.count + 1 }
201
- }
202
- }
203
- }
204
- ```
205
-
206
- There are a couple of new features and concepts introduced here, so we'll go through them one at a time.
207
-
208
-
209
- ### Model View Intent (MVI) Architecture
210
-
211
- Sygnal, being based on Cycle.js, uses the Model View Intent (MVI) philosophy. This approach breaks application logic into the three pieces it gets its name from, and those can be described as:
212
- - **Model**: What the component can do. This piece doesn't actually perform any actions, but instead encodes instructions to send to an appropriate driver when a particular action is to be done.
213
- - **Intent**: When the component should do something. This piece looks at incoming signals from driver 'sources', and determines when specific actions should be kicked off.
214
- - **View**: Exaclty as it sounds, this is how the component should be displayed, and is handled by the main component function.
215
-
216
- An easy way to think about this is the Model tells you **what** a component does, the Intent tells you **when** a component does something, and the View tells you **where** everything is displayed. Finally, outside of the component itself, drivers define **how** anything is done.
217
-
218
-
219
- ### The .intent Property
220
-
221
- Looking at the example component above, the first new thing we've introduced is the .intent property on RootComponent. Like was said in the last section, this is where we use signals we get
222
- from driver 'sources' to determine when to perform an 'action'.
223
-
224
- In Sygnal, an 'action' is simply a name you give to anything you want the component to do. You can make your action names as descriptive as you like, and they can be any valid javascript property name.
225
-
226
- The .intent property is always a function, and gets one argument containing all drivers available to the component. This function should return an object with 'action' names as properties, and an 'Observable' that fires when that action should run as the value (don't worry if you're not familiar or are uncomfortable with Observables, Sygnal minimizes the complexity, and we'll cover what you need to know below).
227
-
228
- > By default all Sygnal applications get STATE, DOM, EVENTS, and LOG drivers. In addition there are props$, children$, context$, and CHILD pseudo-drivers that are useful in more advanced cases.
229
-
230
- In the example component above, we get the DOM driver source using destructuring, and we return an action list object showing that the INCREMENT action should run whenever an HTML element
231
- with a class name of '.increment-button' is clicked.
232
-
233
- > The DOM driver's `.select()` method takes any valid CSS selector, and will find any HTML elements in the component's view that match it.
234
- > The selector is 'isolated' to the current component, and will not match anything outside of the component itself, so there's no need to go crazy with your class names.
235
- >
236
- > Once you've used `DOM.select()`, you can then use `.events()` to pick an event to listen to. The result is an 'Observable' that fires (emitting a DOM event object) whenever the event happens.
51
+ Counter.initialState = { count: 0 }
237
52
 
238
- You'll notice that we don't say anything here about what should actually happen when the INCREMENT action is triggered. All we care about in the intent function is **when** the action happens.
53
+ // Intent: WHEN things happen
54
+ Counter.intent = ({ DOM }) => ({
55
+ INCREMENT: DOM.select('.increment').events('click'),
56
+ DECREMENT: DOM.select('.decrement').events('click')
57
+ })
239
58
 
240
- This `.intent` function
241
- ```javascript
242
- RootComponent.intent = ({ DOM }) => {
243
- return {
244
- INCREMENT: DOM.select('.increment-button').events('click')
245
- }
59
+ // Model: WHAT happens
60
+ Counter.model = {
61
+ INCREMENT: (state) => ({ count: state.count + 1 }),
62
+ DECREMENT: (state) => ({ count: state.count - 1 })
246
63
  }
247
64
  ```
248
- can be read as: `Run the INCREMENT action whenever a DOM element with a class of .increment-button is clicked`
249
65
 
250
- > A key difference you should pay attention to already is that in Sygnal you **never** define event handlers of any kind in the HTML. Any and all DOM events (or any other events for that matter) will always be defined in the .intent function using the DOM driver source.
251
- > This is a defining feature of functional reactive programming where the view shouldn't need to know about anything except the current state, and shouldn't 'do' anything except display things to the user.
252
-
253
-
254
- ### The .model Property
255
-
256
- Now that we've created an `INCREMENT` action and determined when it should run, next we need to tell our component what to do when that action happens. That's where the `.model` property comes in!
66
+ Start it with `run()`:
257
67
 
258
68
  ```javascript
259
- RootComponent.model = {
260
- INCREMENT: {
261
- STATE: (state) => {
262
- return { count: state.count + 1 }
263
- }
264
- }
265
- }
266
- ```
267
-
268
- The `.model` property is just an object with entries for each `action`. There should be one `action` entry for every `action` defined in `.intent`.
269
-
270
- Each `action` entry then gets set with an object listing each `driver` that will be used for that `action`, along with what should be sent to that driver to tell it what to do.
271
-
272
- In this case we're updating the state, so we use the `STATE` driver, and send it a 'reducer' function to tell the `STATE` driver how to update the state. The reducer function gets the current state, and should return what the new state should be after the action is completed.
69
+ import { run } from 'sygnal'
70
+ import Counter from './Counter.jsx'
273
71
 
274
- > It may seem unintuitive to use a 'reducer' function to set the state... why not just update it directly!?
275
- >
276
- > This is how we keep components 'pure'. By delegating even the simple task of updating the `count` state to the `STATE` driver, we guarantee that the component itself has absolutely no side effects, and can be recreated or rerendered at any time without causing unintended concequences in our application.
277
- >
278
- > React handles these situations through hooks like `useState()` or `useEffect()`, but Sygnal takes it even further by enforcing that ALL side effects happen in drivers, which significantly reduces the types of bugs you run into in writing components.
279
-
280
- Updating state is such a common task in Sygnal applications that there's a shorthand way of doing it... if you provide a reducer function directly after the `action` name, Sygnal will assume you are sending it to the `STATE` driver. So you can rewrite our `.model` as:
281
-
282
- ```javascript
283
- RootComponent.model = {
284
- // reducer functions passed to an action name will automatically be sent to the STATE driver
285
- INCREMENT: (state) => {
286
- return { count: state.count + 1 }
287
- }
288
-
289
- // you can make this even shorter using the shortened arrow function object return syntax like:
290
- // INCREMENT: (state) => ({ count: state.count + 1 })
291
- }
72
+ run(Counter)
292
73
  ```
293
74
 
294
- > The overly clever among you may notice that we never return anything to the DOM driver sink. Sygnal assumes that anything returned by the main component function is going to DOM, and handles it automatically. You should never send anything to the DOM driver sink through an action. Just update the state, and use that in the main component function to change what's rendered.
295
-
296
-
297
- ### Observables
298
-
299
- Sygnal (and Cycle.js) are functional reactive style frameworks, and are powered at the core by 'Observables'.
75
+ That's it. No store setup, no providers, no hooks just a function and some properties.
300
76
 
301
- There are many descriptions and explanations of Oservables out there, but the simplest one is just that they are like Promises that can fire/resolve more than once.
302
-
303
- For example, where with a Promise like: `somePromise.then(value => console.log(value))` the `.then()` will only ever run once. With an Observable like: `someObservable.map(value => console.log(value))` the `.map()` can run over and over again.
304
-
305
- Observables are a deep topic, and bring a lot of powerful abilities to programming, but for the purposes of writing Sygnal components, because Sygnal (and Cycle.js) handle all of the complexities for you behind the scenes, just thinking of them as "Promises that fire many times" is good enough to get us going.
306
-
307
- > You'll also hear Observables referred to as 'streams', which is another good way to think of them, but for anyone whose worked with Node.js streams before, that term might cause PTSD. Observables are much easier to work with than traditional streams, and never have the same problems like back-pressure to deal with.
77
+ ## Built on Cycle.js
308
78
 
309
- Tying it back into our example from above, in the `.intent` function we got the button click events using:
79
+ Sygnal is built on top of Cycle.js which is a functional reactive coding framework that asks "what if the user was a function?"
310
80
 
311
- ```javascript
312
- DOM.select('.increment-button').events('click')
313
- ```
81
+ It is worth reading the summary on the [Cycle.js homepage](https://cycle.js.org/), but essentially Cycle.js allows you to write simple, concise, extensible, and testable code using a functional reactive style, and helps ensure that all side-effects are isolated away from your component code.
314
82
 
315
- This will result in an Observable that fires every time the button is clicked. You might think, "That's great, but you don't look like you're doing anything when it fires!". Good observation! We don't do anything here because we pass this Observable back to Sygnal, and it will take care of watching our Observable, and doing things when needed. The only thing you'll ever need to get good at to build Sygnal components is 'finding' or 'composing' the Observables to pass to `actions`, and in most cases that's done with a simple call to one of the driver source objects like we did with DOM.
83
+ Sygnal takes it a step further, and makes it easy to write arbitrarily complex applications that have all of the Cycle.js benefits, but with a much easier learning curve, and virtually no complex plumbing or boiler plate code.
316
84
 
317
- > If you follow the evolution of Javascript frameworks, then you've almost certainly heard about Observables, RxJS, or more recently Signals being incorporated more and more into popular frameworks. These concepts can be very confusing, and as powerful as they are, a common view is that using them can increase learning curves and make debugging harder. There is truth to that, but Sygnal does most of the heavy lifting for you, and adds easy to understand and actionable debug logs and error messages to keep the pain as small as possible.
318
- >
319
- > Sygnal uses an Observable library called [xstream](https://github.com/staltz/xstream "xstream GitHub page") which was written by the same team that created Cycle.js, and is specifically tailored to work well with components, and is extremely small and fast. We may add support for other Observable libraries like RxJS, most.js, or Bacon in the future, but unless you have a specific need for those other libraries, xtream is almost always the best choice anyways.
85
+ ## Key Features
320
86
 
87
+ ### Model-View-Intent Architecture
321
88
 
322
- ## More About State
89
+ Separate **what** your component does (Model), **when** it does it (Intent), and **how** it's displayed (View). All side effects are delegated to drivers, keeping your components 100% pure.
323
90
 
324
91
  ### Monolithic State
325
92
 
326
- Sygnal applications use a monolithic combined state for the entire application. Components do not get independent state. In React, this is similar to how Redux works, but to anyone who just cringed a bit, Sygnal makes this ridiculously easy. There's no painful setup or boilerplate involved, the only added complication over storing state in your components is keeping track of the application state tree... which you should be doing anyways.
327
-
328
- A huge benefit of using monolithic state is that things like adding 'undo' features, or re-initializing the application after a page reload become trivial to implement: just update the state to the previous value, and all components will fall in line correctly.
329
-
330
-
331
- ### Managing Subcomponent State
332
-
333
- By default all components get the entire application state, but this is usually not what you want. First, it can cause you to 'mine' for the right state using statements like:
334
-
335
- ```javascript
336
- // it would be a pain in the butt to have to do something like this everywhere...
337
- const valueImLookingFor = state.items[index].otherProperty[index2].item.value
338
- ```
339
- And second, it would be difficult to create reusable components if every component had to know the shape of your entire application state to work.
340
-
341
- Sygnal solves this problem by letting you specify what part of the state tree to pass to each component. In the simplest case this is just a property on the state, and can be done by setting the 'state' property on the component to the state property name when you add it to the parent component's view:
342
-
343
- ```javascript
344
- // assuming the application state looks like
345
- // { someStateProperty: 'Hi There!' }
346
- // you can limit MyComponent's state to someStateProperty (becomes the 'root' state for the component) like this
347
- function RootComponent() {
348
- return (
349
- <div>
350
- <MyComponent state="someStateProperty" />
351
- </div>
352
- )
353
- }
354
- ```
355
-
356
- In this example, anytime you access the `state` within MyComponent, it will be whatever the value of `someStateProperty` is on the application state.
357
- And if you update the state in one of the component's `actions` in `.model`, it will update the value of `someStateProperty` as if it was the root state:
358
-
359
- ```javascript
360
- // When SOME_ACTION is triggered inside MyComponent
361
- // the value of someStateProperty will be updated in
362
- // the RootComponent's state
363
- MyComponent.model = {
364
- SOME_ACTION: (state) => 'See you later!'
365
- }
366
- ```
367
-
368
- > NOTE: If you specify a name that doesn't exist on the current state, it will just get added to the state if/when the component updates it's state.
369
-
93
+ Automatic application-level state management with no setup. Trivial undo/redo, state restoration, and time-travel debugging.
370
94
 
371
- ### Managing Subcomponent State (Advanced)
95
+ ### Collections
372
96
 
373
- In the majority of cases, either inheriting the parent component's state, or choosing a property on the current state to pass on will work great.
374
- For more advanced cases, Sygnal provides two useful features to help:
375
- - `.context`
376
- - State Lenses
97
+ Render dynamic lists with filtering and sorting built in:
377
98
 
378
-
379
- Adding a `.context` property to a component allows you to set values that should be passed to ALL subcomponents belonging to it,
380
- regardless of how deep they are in the hierarchy. These values are exposed on the `context` view parameter, and in the `extras` of any `action` reducer (the 4th argument):
381
-
382
- ```javascript
383
- // This will set a 'someContext' context value on all subcomponents
384
- // of RootComponent even if they are deeply nested
385
- RootComponent.context = {
386
- someContext: (state) => state.someStateProperty + ' Nice to meet you!'
387
- }
388
-
389
- function MyComponent({ state, context }) {
390
- // this will return 'Hi There! Nice to meet you!' as long as MyComponent is
391
- // somewhere in the subcomponent tree of RootComponent
392
- return (
393
- <div>{ context.someContext }</div>
394
- )
395
- }
396
-
397
- MyComponent.model = {
398
- SOME_ACTION: {
399
- LOG: (state, data, next, extra) => {
400
- // the 4th argument of any reducer in an action contains: context, props, and children
401
- // since we're using the LOG driver sink's reducer, this will print 'Hi There! Nice to meet you!'
402
- // to the browser console whenever the SOME_ACTION action is triggered
403
- return extra.context.someContext
404
- }
405
- }
406
- }
407
- ```
408
-
409
-
410
- Another option you can provide to the `state` property of a component is a State Lense.
411
- A State Lense is just a simple Javascript object with `get` and `set` functions to tell Sygnal how to get the state for the component and set the state back on the parent when this component updates it's own state:
412
-
413
- ```javascript
414
- const sampleLense = {
415
- get: (state) => {
416
- // state here is the parent component's state
417
- // just return whatever you want the subcomponent's state to be
418
- return {
419
- someField: state.someField
420
- otherField: state.otherField
421
- }
422
- },
423
- set: (parentState, newSubcomponentState) => {
424
- // use the newSubcomponentState to calculate and return what the
425
- // new parentState should be after the subcomponent updates
426
- return {
427
- ...parentState,
428
- someField: newSubcomponentState.someField
429
- otherField: newSubcomponentState.otherField
430
- }
431
- }
432
- }
433
-
434
- // use the lense by setting the state property to it when you use a component
435
- function RootComponent() {
436
- return (
437
- <MyComponent state={ sampleLense } />
438
- )
439
- }
99
+ ```jsx
100
+ <collection of={TodoItem} from="items" filter={item => !item.done} sort="name" />
440
101
  ```
441
102
 
442
- > NOTE: State Lenses give you maximum flexibility to handle how state flows from parent to child components, but are rarely actually needed,
443
- > and can be difficult to debug. If you find yourself reaching to State Lenses often, there's probably an easier way to do what you want
444
- > using normal Sygnal features
445
-
446
-
447
-
448
- ### Realistic State Updates
449
-
450
- The examples above are great for getting a basic understanding of state in Sygnal, but in the real world `actions` need more information than is
451
- already in the state to do their job properly. So let's talk about how to feed that data to actions, and access it in reducers.
452
-
453
- We've already dicussed Observables, and how they fire whenever their associated events happen, but one thing we glossed over is that those Observables
454
- can also provide data when they fire (just like Promises).
455
-
456
- For instance, in the button click example from above, the Observable created with `DOM.select('.increment-button').events('click')` will return a
457
- DOM click event every time it fires. As it stands, because we haven't done anything to it, that DOM click event will be sent 'as is' to the second argument
458
- of any reducers triggered by `actions` we pass the Observable to.
459
-
460
- So if we change the code to:
461
-
462
- ```javascript
463
- function RootComponent() {
464
- return <button className="increment-button" data-greeting="Hello!" />
465
- }
103
+ ### Switchable Components
466
104
 
467
- RootComponent.intent = ({ DOM }) => {
468
- return {
469
- INCREMENT: DOM.select('.increment-button').events('click')
470
- }
471
- }
105
+ Swap between components based on state:
472
106
 
473
- RootComponent.model = {
474
- INCREMENT: {
475
- LOG: (state, data) => {
476
- return data
477
- }
478
- }
479
- }
107
+ ```jsx
108
+ <switchable of={{ home: HomePage, settings: SettingsPage }} current={state.activeTab} />
480
109
  ```
481
110
 
482
- The DOM click event will be logged to the browser console.
111
+ ### Form Handling
483
112
 
484
- There are two very useful Observable methods we can use to change this default behavior. The first is `.mapTo()`, which exactly how it sounds,
485
- maps the value returned from an Observable to the value you designate, so `DOM.select('.increment-button').events('click').mapTo('CLICK!')`
486
- causes our component to log 'CLICK!' to the browser console instead of the DOM click event like before.
113
+ Extract form values without the plumbing:
487
114
 
488
- The other useful Observable method is `.map()`, which instead of just always returning the same value, allows us to 'mutate' the current value
489
- before it gets sent on. Say we needed to get the `data-greeting` property from the button that was clicked. We can do that like:
115
+ ```jsx
116
+ import { processForm } from 'sygnal'
490
117
 
491
- ```javascript
492
- DOM.select('.increment-button').events('click').map(event => event.target.dataset['greeting'])
118
+ MyForm.intent = ({ DOM }) => ({
119
+ SUBMITTED: processForm(DOM.select('.my-form'), { events: 'submit' })
120
+ })
493
121
  ```
494
122
 
495
- If we use this for the INCREMENT action in the component .intent, then 'Hello!' will get logged to the browser console whenever the button is clicked.
496
-
497
-
498
- ### Do Multiple Things For One Action
123
+ ### Custom Drivers
499
124
 
500
- In some cases you'll need to do more than one thing when a single 'action' is triggered. Sygnal provides a couple of tools to help with that.
501
-
502
- In the case that you want to do multiple separate things with different drivers, you can simply add extra entries for each driver in the action
503
- object:
125
+ Wrap any async operation as a driver:
504
126
 
505
127
  ```javascript
506
- // This will both update the count in the state,
507
- // and print 'Updating count' to the browser console
508
- // whenever SOME_ACTION is triggered
509
- MyComponent.model = {
510
- SOME_ACTION: {
511
- STATE: (state) => ({ ...state, count: state.count + 1 }),
512
- LOG: (state) => 'Updating count'
513
- }
514
- }
515
- ```
128
+ import { driverFromAsync } from 'sygnal'
516
129
 
517
- In other cases, you might want to tigger other actions to happen based on certain conditions. For that, Sygnal provides a `next()` function as
518
- the 3rd argument to all 'reducer' functions which can be used to manually trigger any other action in your component:
130
+ const apiDriver = driverFromAsync(async (url) => {
131
+ const res = await fetch(url)
132
+ return res.json()
133
+ }, { selector: 'endpoint', args: 'url', return: 'data' })
519
134
 
520
- ```javascript
521
- // When SOME_ACTION is triggered...
522
- // If 'logIt' on the state is true, then the next() function triggers
523
- // the ANOTHER_ACTION action to be run with the 'data' set to 'Log Me!'
524
- // since that action uses the LOG driver sink, and passes 'data',
525
- // 'Log Me!' will be printed to the browser console
526
- MyComponent.model = {
527
- SOME_ACTION: (state, data, next) => {
528
- if (state.logIt === true) next('ANOTHER_ACTION', 'Log Me!')
529
- return { ...state, itGotLogged: true }
530
- },
531
- ANOTHER_ACTION: {
532
- LOG: (state, data) => data
533
- // setting any driver sink entry to 'true' will cause anything in data
534
- // to be passed on directly, so the following is exactly the same
535
- // LOG: true
536
- }
537
- }
135
+ run(RootComponent, { API: apiDriver })
538
136
  ```
539
137
 
138
+ ### Hot Module Replacement
540
139
 
541
- ## Common Tasks Made Easy
542
-
543
- There are a few things that almost any application will have that can be challenging for people new to functional reactive programming to do.
544
- To keep you from pulling your hair out, and even make these tasks look easy, Sygnal provides several features and helpers.
545
-
546
- ### Collections of Components
547
-
548
- Pretty much any application will at some point will have lists, arrays, or some other place where many of the same component is shown,
549
- and may even need to dynamically change the list size, filter what's shown, or sort items on some criteria.
550
- For this, Sygnal provides a built-in `<collection>` element that takes a provided component and an array on your state
551
- and handles everything else for you.
552
-
553
- In this example, a new ListItemComponent will be created for each item in the `list` state property. It will be filtered to only show items
554
- where showMe is true, and will be sorted by name.
555
-
556
- Each instance created will have it's associated item as its state, and if it updates state, the item in the list will get updated properly as well.
557
- An item can even delete itself by setting its own state to 'undefined', in which case it will be automatically removed from the array on state.
140
+ State-preserving HMR out of the box:
558
141
 
559
142
  ```javascript
560
- function RootComponent({ state }) {
561
- return (
562
- <div>
563
- <collection of={ ListItemComponent } from="list" filter={ item => item.showMe === true } sort="name" />
564
- </div>
565
- )
566
- }
143
+ const { hmr, dispose } = run(RootComponent)
567
144
 
568
- RootComponent.initialState = {
569
- list: [
570
- { name: 'Bob', showMe: true },
571
- { name: 'Sarah', showMe: true },
572
- { name: 'David', showMe: true },
573
- { name: 'Joe', showMe: false }
574
- ]
575
- }
576
-
577
- function ListItemComponent({ state }) => {
578
- return <div>{ state.name }</div>
145
+ if (import.meta.hot) {
146
+ import.meta.hot.accept('./RootComponent.jsx', hmr)
147
+ import.meta.hot.dispose(dispose)
579
148
  }
580
149
  ```
581
- > NOTE: notice `collection` is lower case. This is to avoid problems with JSX conversion.
582
-
583
-
584
- ### Switchable Components
585
150
 
586
- Another common use case is to show different content based on some criteria, like changing things when a different tab is clicked.
587
- For this, Sygnal provides a built-in `<switchable>` element that takes an object mapping names to components, and the name of the
588
- component to show now.
589
-
590
- In this example, clicking the buttons will switch back and forth between showing FirstComponent or SecondComponent by changing
591
- the value of the `whichItemToShow` state.
151
+ ### Astro Integration
592
152
 
593
153
  ```javascript
594
- function RootComponent({ state }) {
595
- return (
596
- <div>
597
- <button className="show-item1">Item 1</button>
598
- <button className="show-item2">Item 2</button>
599
- <switchable of={ item1: FirstComponent, item2: SecondComponent } current={ state.whichItemToShow } />
600
- </div>
601
- )
602
- }
603
-
604
- RootComponent.intent = ({ DOM }) => {
605
- const item1$ = DOM.select('.show-item1').events('click').mapTo('item1')
606
- const item2$ = DOM.select('.show-item2').events('click').mapTo('item2')
607
-
608
- return {
609
- // xs.merge() is an Observable function that merges multiple Observables together
610
- // the result is a new Observable that fires whenever any of the original ones do
611
- // and forwards the data on. xs can be imported from sygnal.
612
- SET_ITEM: xs.merge(item1$, item2$)
613
- }
614
- }
615
-
616
- RootComponent.model = {
617
- SET_ITEM: (state, data) => ({ ...state, whichItemToShow: data })
618
- }
619
- ```
620
-
621
-
622
- ### The processForm() Function
623
-
624
- Another very common task in web pages and browser applications is to work with form inputs. Unfortunately, the logic and stream plumbing required to do this routine task can be challenging to developers new to observables (and is frustrating even for most veterans). Sygnal's processForm() helper function takes any HTML form element, and automatically extracts the values from all input fields contained within it. By default processForm() listens to both 'input' and 'submit' events, but can be configured to listen to any combination of standard or custom events on the form itself or its inputs.
625
-
626
- The Observable from `processForm` always returns objects with the current value of every field in the form along with the name of the JS event that initiated it, so the following will print something like:
627
-
628
- ```javascript
629
- {
630
- 'first-name': 'Bob',
631
- 'last-name': 'Smith',
632
- eventType: 'submit'
633
- }
154
+ // astro.config.mjs
155
+ import sygnal from 'sygnal/astro'
156
+ export default defineConfig({ integrations: [sygnal()] })
634
157
  ```
635
158
 
636
- ```javascript
637
- import { processForm } from 'sygnal'
638
-
639
- function MyComponent({ state }) {
640
- return (
641
- <form className="my-form">
642
- <input name="first-name" />
643
- <input name="last-name" />
644
- <button type="submit">Submit</button>
645
- </form>
646
- )
647
- }
648
-
649
- MyComponent.intent = ({ DOM }) => {
650
- const submit$ = processForm(DOM.select('.my-form'), { events: 'submit' })
651
-
652
- return {
653
- HANDLE_FORM: submit$
654
- }
655
- }
656
-
657
- MyComponent.model = {
658
- HANDLE_FORM: {
659
- LOG: (state, data) => data
660
- }
661
- }
159
+ ```astro
160
+ ---
161
+ import Counter from '../components/Counter.jsx'
162
+ ---
163
+ <Counter client:load />
662
164
  ```
663
165
 
166
+ ### TypeScript Support
664
167
 
665
- ## Prerequisites / Using a Starter Template
168
+ Full type definitions included:
666
169
 
667
- For plain Javascript usage, Sygnal has no prerequisites as all dependencies are pre-bundled.
668
-
669
- To bootstrap a minimal Sygnal application using Vite and that supports JSX:
670
-
671
- ```bash
672
- npx degit tpresley/sygnal-template my-awesome-app
673
- cd my-awesome-app
674
- npm install
675
- npm run dev
676
- ```
170
+ ```tsx
171
+ import type { RootComponent } from 'sygnal'
677
172
 
678
- To build an optimized production ready version of your app:
173
+ type AppState = { count: number }
174
+ type AppActions = { INCREMENT: null }
679
175
 
680
- ```bash
681
- npm run build
176
+ const App: RootComponent<AppState, {}, AppActions> = ({ state }) => (
177
+ <div>{state.count}</div>
178
+ )
682
179
  ```
683
180
 
684
- The results will be in the 'dist' folder, and you can serve it locally by running:
181
+ ## Bundler Setup (JSX)
685
182
 
686
- ```bash
687
- npm run preview
688
- ```
689
-
690
- Alternatively, you can use any other bundler of your choice (Webpack, Babel, Rollup, etc.). To use JSX in your components while using alternative bundlers, you will need to configure your bundler to use Sygnal's JSX pragma. This is slightly different for each bundler, but looks generally like:
183
+ For Vite:
691
184
 
692
185
  ```javascript
693
- // this example is for Vite or esbuild, but most bundlers have options similar to this for handling JSX transpiling
694
- {
695
- ...,
186
+ // vite.config.js
187
+ export default defineConfig({
696
188
  esbuild: {
697
- // add the import for Sygnal's JSX and Fragment handler to the top of each .jsx and .tsx page automatically
698
189
  jsxInject: `import { jsx, Fragment } from 'sygnal/jsx'`,
699
- // tell the transpiler to use Sygnal's 'jsx' funtion to render JSX elements
700
- jsxFactory: `jsx`,
701
- // tell the transpiler to use Sygnal's 'Fragment' funtion to render JSX fragments (<>...</>)
190
+ jsxFactory: 'jsx',
702
191
  jsxFragment: 'Fragment'
703
- },
704
- }
705
- ```
706
-
707
- NOTE: Some minifiers will cause JSX fragments to fail by renaming the Fragment pragma function. This can be fixed by preventing renaming of 'Fragment'. For Vite this is done by installing terser, and adding the following to your vite.config.js file:
708
-
709
- ```javascript
710
- {
711
- ...,
712
- build: {
713
- minify: 'terser',
714
- terserOptions: {
715
- mangle: {
716
- reserved: ['Fragment'],
717
- },
718
- }
719
- },
720
- }
721
- ```
722
-
723
- ## More Examples
724
-
725
- ### Two Way Binding
726
-
727
- ```javascript
728
- function TwoWay({ state }) {
729
- return (
730
- <div>
731
- <h1>Hello { state.name }</h1>
732
- {/* set the 'value' of the input to the current state */}
733
- <input className="name" value={ state.name } />
734
- </div>
735
- )
736
- }
737
-
738
- TwoWay.initialState = { name: 'World!' }
739
-
740
- TwoWay.intent = ({ DOM }) => {
741
- return {
742
- // select the input DOM element using it's class name
743
- // then map changes to the value ('input' event) to extract the value
744
- // that value will then be passed to the 2nd parameter of reducers in 'model'
745
- CHANGE_NAME: DOM.select('.name').events('input').map(e => e.target.value)
746
- }
747
- }
748
-
749
- TwoWay.model = {
750
- // update the name in the state whenever the 'CHANGE_NAME' action is triggered
751
- // this time we use the 2nd parameter of the reducer function which gets the value passed
752
- // by the stream that triggered the action
753
- CHANGE_NAME: (state, data) => {
754
- return { name: data }
755
192
  }
756
- }
193
+ })
757
194
  ```
758
195
 
759
-
760
- ### Multiple Actions
196
+ Without JSX, use `h()`:
761
197
 
762
198
  ```javascript
763
- // import the xtream observable library so we can do some stream operations
764
- import { xs } from 'sygnal'
765
-
766
- function Counter({ state }) {
767
- return (
768
- <div>
769
- <h1>Current Count: { state.count }</h1>
770
- <input type="button" className="increment" value="+" />
771
- <input type="button" className="decrement" value="-" />
772
- <input className="number" value={ state.count } />
773
- </div>
774
- )
775
- }
776
-
777
- Counter.initialState = { count: 0 }
778
-
779
- Counter.intent = ({ DOM }) => {
780
- // rather than pass Observables directly to the actions, it is sometimes helpful
781
- // to collect them in variables first
782
- // it is convention (but not required) to name variables containing Observables with a trailing '$'
783
- // the 'mapTo' function causes the Observable to emit the specified value whenever the stream fires
784
- // so the increment$ stream will emit a '1' and the decrement$ stream a '-1' whenever their
785
- // respective buttons are pressed, and as usual those values will be passed to the 2nd parameter
786
- // of the reducer functions in the 'model'
787
- const increment$ = DOM.select('.increment').events('click').mapTo(1)
788
- const decrement$ = DOM.select('.decrement').events('click').mapTo(-1)
789
- const setCount$ = DOM.select('.number').events('input').map(e => e.target.value)
790
-
791
- return {
792
- // the 'merge' function merges the events from all streams passed to it
793
- // this causes the 'INCREMENT' action to fire when either the increment$ or decrement$
794
- // streams fire, and will pass the value that the stream emeits (1 or -1 in this case)
795
- INCREMENT: xs.merge(increment$, decrement$),
796
- SET_COUNT: setCount$
797
- }
798
- }
799
-
800
- Counter.model = {
801
- // add the value passed from the stream that triggered the action to the current count
802
- // this will either be 1 or -1, so will increment or decrement the count accordingly
803
- INCREMENT: (state, data) => ({ count: state.count + data }),
804
- SET_COUNT: (state, data) => ({ count: parseInt(data || 0) })
805
- }
199
+ import { h } from 'sygnal'
200
+ h('div', [h('h1', 'Hello'), h('button.btn', 'Click')])
806
201
  ```
807
202
 
203
+ See the [Guide](./docs/guide.md#bundler-configuration) for other bundlers.
808
204
 
205
+ ## Examples
809
206
 
810
- ## More Documentation To Come...
207
+ - **[HMR Smoke Test](./examples/hmr-smoke)** — Minimal counter with HMR
208
+ - **[TypeScript 2048](./examples/ts-example-2048)** — Full game in TypeScript
209
+ - **[AI Discussion Panel](./examples/ai-panel-spa)** — Complex SPA with custom drivers
210
+ - **[Astro Integration](./examples/astro-smoke)** — Sygnal in Astro
211
+ - **[Sygnal ToDoMVC](https://github.com/tpresley/sygnal-todomvc)** ([Live Demo](https://tpresley.github.io/sygnal-todomvc/)) — TodoMVC implementation
212
+ - **[Sygnal 2048](https://github.com/tpresley/sygnal-2048)** ([Live Demo](https://tpresley.github.io/sygnal-2048/)) — 2048 game
213
+ - **[Sygnal Calculator](https://github.com/tpresley/sygnal-calculator)** ([Live Demo](https://tpresley.github.io/sygnal-calculator/)) — Simple calculator
811
214
 
812
- Sygnal is the result of several years of building Cycle.js apps, and our attempts to make that process more enjoyable. It has been used for moderate scale production applications, and we are making it available to the world in the hopes it is useful.
215
+ ## License
813
216
 
814
- Until better documentation is available, here are some well-commented projects using most of Sygnal's features:
815
- - Sygnal ToDoMVC ( [GitHub](https://github.com/tpresley/sygnal-todomvc) | [Live Demo](https://tpresley.github.io/sygnal-todomvc/) ) - The [ToDoMVC](https://todomvc.com/) framework comparison app implemented in Sygnal
816
- - Sygnal 2048 ( [GitHub](https://github.com/tpresley/sygnal-2048) | [Live Demo](https://tpresley.github.io/sygnal-2048/) ) - The [2048 Game](https://github.com/gabrielecirulli/2048 "Original 2048 GitHub repo") implemeted in Sygnal
817
- - Sygnal Calculator ( [GitHub](https://github.com/tpresley/sygnal-calculator) | [Live Demo](https://tpresley.github.io/sygnal-calculator/) ) - A simple calculator implemeted in Sygnal
217
+ MIT