sygnal 2.9.4 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,349 +1,814 @@
1
1
  # Sygnal
2
2
 
3
- An intuitive framework for building fast and small components or applications based on Cycle.js
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
7
 
6
- ## Cycle.js
8
+ 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
+ - 100% pure components with absolutely no side effects
10
+ - No need for component state management (it's handled automatically at the application level)
11
+ - Small bundle sizes
12
+ - Fast build times
13
+ - Fast rendering
14
+ - Close to zero boiler plate code
7
15
 
8
- Cycle.js is a functional reactive coding framework that asks 'what if the user was a function?'
16
+ ## Built on Cycle.js
9
17
 
10
- 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.
11
-
12
- ## Sygnal
13
-
14
- Sygnal makes building Cycle.js apps and components much easier by handling all of the most complex stream plumbing, and provides a minimally opinionated structure to component code while maintaining full forwards and backwards compatibility with all Cycle.js components whether built with or without Sygnal.
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?'
15
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.
16
21
 
17
- ## Why?
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.
18
23
 
19
- Cycle.js is a powerful and criminally underappreciated framework that despite its many advantages (like fully isolated side-effects, functional reactive style, pure-by-nature components, extremely small number of dependencies, tiny bundle size, and fast performance) can be challenging to build complex applications with due to the high learning curve to understanding functional reactive style programming with observables, and the often complex stream plumbing that is required to layer and connect components together.
24
+ ## Goals of Sygnal
20
25
 
21
- Sygnal provides a structured way to create Cycle.js components that accomplishes several key goals:
26
+ Sygnal provides a structured way to create components that accomplishes several key goals:
22
27
  - Minimize boilerplate
23
- - Provide a simplified way to handle common application tasks
24
- - Handle all stream plumbing between components
28
+ - Provide a simplified way to handle common application tasks without a bloated API
29
+ - Handle all stream plumbing between components automatically
25
30
  - Support arbitrarily complex applications with deep component hierarchies
26
31
  - Reuse the best patterns from popular frameworks like React and Vue while avoiding the pitfalls
27
- - Support pure Javascript, Typescript, and JSX (including fragments)
28
- - Provide application state out of the box, and make it easy to use
29
- - Use reasonable defaults while providing access to low-level Cycle.js functionality wherever possible
30
- - Provide automatic debugging information
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
31
35
  - Work with modern bundlers like Vite, and provide easy application bootstrapping
32
36
 
33
37
 
34
- ## Features
38
+ ## In a Nutshell...
35
39
 
36
- Sygnal provides the following features for quickly building powerful components to build either fully Sygnal based applications, or to be used in combination with existing Cycle.js components.
40
+ Sygnal is easiest to understand by example, so let's walk through some now.
37
41
 
38
- ### The component() Function
39
42
 
40
- Sygnal's component() function is the only thing needed to create a stand-alone component. It takes any of a number of optional parameters, and returns a Cycle.js compatible component (See the [Cycle.js documentation](https://cycle.js.org/getting-started.html "Cycle.js Documentation") for a full description, but essentially this means component() returns a function that accepts Cycle.js 'sources' and returns Cycle.js 'sinks').
43
+ ### Installation
41
44
 
42
- The 3 most common/useful parameters to component() are:
43
- - **model**: an object that maps 'action' names to the commands or reducers that tell Cycle.js drivers **WHAT** to do
44
- - **intent**: a function that receives Cycle.js sources and returns a map of 'action' names to observable streams telling the application **WHEN** that action should happen.
45
- - **view**: a function receiving the current application state and returning virtual DOM elements (using either Preact style h() functions or by using JSX transpiling using snabbdom-pragma)
45
+ To install Sygnal, just use your favorite package manager to grab the `sygnal` package
46
46
 
47
- Essentially the **'model'** parameter determines **WHAT** should happnen, the **'intent'** parameter determines **WHEN** things happen, the **'view'** parameter determines **WHERE** everything is rendered in the browser, and the provided Cycle.js **'drivers'** determine **HOW** things happen.
47
+ ```bash
48
+ npm install sygnal
49
+ ```
48
50
 
49
- Unlike most other popular frameworks, Sygnal (being built on Cycle.js) does not expect or rely on any events or functions being specified in the HTML view. Instead, **ALL** events that the application should respond to (whether a user action, a remote network call, a timer, or any other external event) are detected in the **'intent'** function... the **'view'** is **ONLY** for presentation.
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'`
50
63
 
51
- This strict separation of component logic makes reasoning about how to build the component easier, and makes refactoring and enhancing components a breeze.
64
+ ### React Style Functional Components
52
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).
53
67
 
54
- ### The collection() Function
68
+ The simplest Sygnal component is just a function that returns JSX or vDom:
55
69
 
56
- Sygnal's collection() function is a wrapper for Cycle.js's makeCollection() function (See the [documentation here](https://cycle.js.org/api/state.html#cycle-state-source-usage-how-to-handle-a-dynamic-list-of-nested-components "@cycle/state makeComponent documentation")) that provides an extremely simplified API for creating dynamic lists of components from an array, and automatically grows, shrinks and updates with changes to the state. The collection() function is designed to work 'as is' for the vast majority of use cases, and provides configuration options for more advanced use cases. And in the rare case that collection() is not powerful enough, Sygnal components can seamlessly work with the results of Cycle.js's makeCollection() instead.
70
+ ```javascript
71
+ // Hello World Component
72
+ function MyComponent() {
73
+ return (
74
+ <div>Hello World</div>
75
+ )
76
+ }
77
+ ```
57
78
 
79
+ ### View Parameters
58
80
 
59
- ### The switchable() Function
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).
60
82
 
61
- Sygnal's switchable() function provides an easy way to create a new component that 'switches' between multiple other components (for switching content based on tab or menu navigation for example).
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.
62
84
 
63
- The 'active' component (the component which is made visible) can be set by either providing an observable that emits component names, or by a function that takes the current application state and returns the component name.
85
+ To access any of these values, simply use destructuring in the function arguments:
64
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
+ }
65
98
 
66
- ### The run() Function
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
+ ```
67
111
 
68
- Sygnal's run() function is a wrapper for Cycle.js's run() function with the following additions/defaults:
69
- - Automatically adds application level state (add a 'source' and 'sink' with the name 'STATE')
70
- - Adds a DOM driver (providing user events and accepting new virtual DOM)
71
- - Adds an EVENTS driver to allow easy messaging between components or the entire application
72
- - Adds a LOG driver that simply console.log's any data passed to it
73
- - Looks for and mounts to an HTML element with an id of root (#root)
74
112
 
75
- *NOTE: Sygnal currently only supports xstream as its observable library despite Cycle.js supporting Most and RxJS as well. Support for these alternative observable libraries will be added in the near future.*
113
+ ### Root Component (Starting a Sygnal Application)
76
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:
77
116
 
78
- ### The processForm() Function
117
+ To start an application using the components above, just import run() from Sygnal, and pass RootComponent to it:
79
118
 
80
- A 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.
119
+ ```javascript
120
+ import {run} from 'sygnal'
81
121
 
122
+ // this will attach RootComponent to an HTML element with the id of "root"
123
+ run(RootComponent)
82
124
 
125
+ // to attach to a different element, use the mountPoint option:
126
+ run(RootComponent, {}, { mountPoint: '#css-selector' })
127
+ ```
83
128
 
84
- ## Prerequisites
85
129
 
86
- For plain Javascript usage, Sygnal has no prerequisites as all dependencies are pre-bundled.
130
+ ### State (The Basics)
87
131
 
88
- To bootstrap a minimal Sygnal application using Vite and that supports JSX:
132
+ Every Sygnal application is provided with state which is automatically passed to every component in the hierarchy.
89
133
 
90
- ```bash
91
- npx degit tpresley/sygnal-template my-awesome-app
92
- cd my-awesome-app
93
- npm install
94
- npm run dev
95
- ```
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.
96
135
 
97
- To build an optimized production ready version of your app:
136
+ The easiest way to set the initial state is to augment the main component function with an .initialState property:
98
137
 
99
- ```bash
100
- npm run build
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
+ }
101
150
  ```
102
151
 
103
- The results will be in the 'dist' folder, and you can serve it locally by running:
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
104
153
 
105
- ```bash
106
- npm preview
154
+
155
+ ### Drivers
156
+
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 }) {
170
+ return (
171
+ <div>
172
+ <span>{ state.count }</span>
173
+ <button type="button" className="increment-button" />
174
+ </div>
175
+ )
176
+ }
177
+
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
+ }
107
204
  ```
108
205
 
109
- 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:
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
110
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.
237
+
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.
239
+
240
+ This `.intent` function
111
241
  ```javascript
112
- // this example is for Vite or esbuild, but most bundlers have options similar to this for handling JSX transpiling
113
- {
114
- ...,
115
- esbuild: {
116
- // add the import for Sygnal's JSX and Fragment handler to the top of each .jsx and .tsx page automatically
117
- jsxInject: `import { jsx, Fragment } from 'sygnal/jsx'`,
118
- // tell the transpiler to use Sygnal's 'jsx' funtion to render JSX elements
119
- jsxFactory: `jsx`,
120
- // tell the transpiler to use Sygnal's 'Fragment' funtion to render JSX fragments (<>...</>)
121
- jsxFragment: 'Fragment'
122
- },
242
+ RootComponent.intent = ({ DOM }) => {
243
+ return {
244
+ INCREMENT: DOM.select('.increment-button').events('click')
245
+ }
123
246
  }
124
247
  ```
248
+ can be read as: `Run the INCREMENT action whenever a DOM element with a class of .increment-button is clicked`
125
249
 
126
- 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:
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!
127
257
 
128
258
  ```javascript
129
- {
130
- ...,
131
- build: {
132
- minify: 'terser',
133
- terserOptions: {
134
- mangle: {
135
- reserved: ['Fragment'],
136
- },
259
+ RootComponent.model = {
260
+ INCREMENT: {
261
+ STATE: (state) => {
262
+ return { count: state.count + 1 }
137
263
  }
138
- },
264
+ }
139
265
  }
140
266
  ```
141
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.
142
273
 
143
- ## Initialization
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.
144
279
 
145
- If you used the Vite based sygnal-template above, then the initialization code was already added to a script block in index.html for you. Otherwise, you can initialize a Sygnal app by adding the following to your project entry point (usually index.js):
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:
146
281
 
147
282
  ```javascript
148
- import { run } from 'sygnal'
149
- // replace the following line with your app's root component
150
- import App from './app'
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
+ }
151
288
 
152
- run(App) // <-- automatically binds to a #root HTML element (make sure you have an element with id="root" or the app won't start)
289
+ // you can make this even shorter using the shortened arrow function object return syntax like:
290
+ // INCREMENT: (state) => ({ count: state.count + 1 })
291
+ }
153
292
  ```
154
293
 
155
- Now you're all set to create components! If you used the Vite based sygnal-template above then you can start a Vite dev server that watches for file changes with:
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.
156
295
 
157
- ```bash
158
- npm run dev
159
- ```
160
296
 
297
+ ### Observables
161
298
 
162
- ## Basic Examples
299
+ Sygnal (and Cycle.js) are functional reactive style frameworks, and are powered at the core by 'Observables'.
163
300
 
164
- ### Hello World
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.
165
302
 
166
- The most basic (and not very useful) component
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.
167
304
 
168
- ```javascript
169
- // app.jsx
170
- import { component } from 'sygnal'
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.
171
306
 
172
- export default component({
173
- view: () => <h1>Hello World!</h1>
174
- })
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.
308
+
309
+ Tying it back into our example from above, in the `.intent` function we got the button click events using:
310
+
311
+ ```javascript
312
+ DOM.select('.increment-button').events('click')
175
313
  ```
176
314
 
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.
316
+
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.
320
+
321
+
322
+ ## More About State
323
+
324
+ ### Monolithic State
325
+
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.
177
329
 
178
- ### Using state (basic)
179
330
 
180
- All Sygnal components get state out of the box. Sub or child components will get state passed from their parent component, but the root component will need an initial state to get things rolling.
331
+ ### Managing Subcomponent State
181
332
 
182
- This can be provided using the 'initialState' parameter of component().
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:
183
334
 
184
335
  ```javascript
185
- // app.jsx
186
- import { component } from 'sygnal'
187
-
188
- export default component({
189
- initialState: { who: 'World!' },
190
- view: ({ state }) => <h1>Hello { state.who }</h1>
191
- // if you prefer not to use JSX, the above is equivalent to:
192
- //
193
- // view: ({ state }) => h('h2', `Hello ${ state.who }`)
194
- //
195
- // but you will need to add "import { h } from 'sygnal'" to the top of your file
196
- })
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
197
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.
198
340
 
199
- As shown here, the current state of the application (equal to the value of 'initialState' for now) will be passed to the view() function, and can be used in any valid Javascript/JSX syntax that results in virtual DOM.
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:
200
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
+ ```
201
367
 
202
- ### DOM Events
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.
203
369
 
204
- To make components capable of responding to users interacting with the DOM, you will need to add the 'model' and 'intent' parameters.
205
370
 
206
- The 'model' parameter is an object that maps 'action' names to what should be done when that action happens.
371
+ ### Managing Subcomponent State (Advanced)
207
372
 
208
- The 'intent' parameter is a function that takes Cycle.js 'sources' and returns an object mapping 'action' names to streams/observables which fire/emit when that action should occur.
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
209
377
 
210
- This sounds more complicated than it is... basically the 'model' answers **WHAT** can/should happen, and the 'intent' answers **WHEN** those things will happen.
211
378
 
212
- To illustrate, here's a basic counter that increments when the user clicks anywhere in the page:
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):
213
381
 
214
382
  ```javascript
215
- // app.jsx
216
- import { component } from 'sygnal'
217
-
218
- export default component({
219
- // initialize the count to 0
220
- initialState: { count: 0 },
221
- model: {
222
- // when the 'INCREMENT' action happens, run this 'reducer' function
223
- // which takes the current state and returns the updated state,
224
- // in this case incrementing the count by 1
225
- INCREMENT: (state) => {
226
- return { count: state.count + 1 }
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
227
404
  }
228
- },
229
- // the 'sources' passed to intent() is an object containing an entry for each Cycle.js 'driver'
230
- // Sygnal automatically adds STATE, DOM, EVENTS, and LOG drivers and their resulting sources and sinks
231
- // the DOM source allows you to select DOM elements by any valid CSS selector, and listen for any DOM events
232
- // because we map document click events to the 'INCREMENT' action, it will cause the 'INCREMENT' action in 'model' to fire
233
- // whenever the document is clicked
234
- intent: (sources) => {
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
235
418
  return {
236
- INCREMENT: sources.DOM.select('document').events('click')
419
+ someField: state.someField
420
+ otherField: state.otherField
237
421
  }
238
422
  },
239
- // every time the state is changed, the view will automatically be efficiently rerendered (only DOM elements that have changed will be impacted)
240
- view: ({ state }) => <h1>Current Count: { state.count }</h1>
241
- })
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
+ }
242
440
  ```
243
441
 
244
- *NOTE: action names (like INCREMENT in the above example) can be any valid Javascript object key name*
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
+
245
446
 
246
447
 
247
- ### DOM Events (part 2)
448
+ ### Realistic State Updates
248
449
 
249
- Now let's improve our Hello World app with 2-way binding on an input field
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:
250
461
 
251
462
  ```javascript
252
- // app.jsx
253
- import { component } from 'sygnal'
254
-
255
- export default component({
256
- // initial name
257
- initialState: { name: 'World!' },
258
- model: {
259
- // update the name in the state whenever the 'CHANGE_NAME' action is triggered
260
- // this time we use the 2nd parameter of the reducer function which gets the value passed
261
- // by the stream that triggered the action
262
- CHANGE_NAME: (state, data) => {
263
- return { name: data }
264
- }
265
- },
266
- // it's usually more convenient to use destructuring to 'get' the individual sources you need, like DOM in this case
267
- intent: ({ DOM }) => {
268
- return {
269
- // select the input DOM element using it's class name
270
- // then map changes to the value ('input' event) to extract the value
271
- // that value will then be passed to the 2nd parameter of reducers in 'model'
272
- CHANGE_NAME: DOM.select('.name').events('input').map(e => e.target.value)
463
+ function RootComponent() {
464
+ return <button className="increment-button" data-greeting="Hello!" />
465
+ }
466
+
467
+ RootComponent.intent = ({ DOM }) => {
468
+ return {
469
+ INCREMENT: DOM.select('.increment-button').events('click')
470
+ }
471
+ }
472
+
473
+ RootComponent.model = {
474
+ INCREMENT: {
475
+ LOG: (state, data) => {
476
+ return data
273
477
  }
478
+ }
479
+ }
480
+ ```
481
+
482
+ The DOM click event will be logged to the browser console.
483
+
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.
487
+
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:
490
+
491
+ ```javascript
492
+ DOM.select('.increment-button').events('click').map(event => event.target.dataset['greeting'])
493
+ ```
494
+
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
499
+
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:
504
+
505
+ ```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
+ ```
516
+
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:
519
+
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 }
274
530
  },
275
- view: ({ state }) => {
276
- return (
277
- <div>
278
- <h1>Hello { state.name }</h1>
279
- {/* set the 'value' of the input to the current state */}
280
- <input className="name" value={ state.name } />
281
- </div>
282
- )
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
283
536
  }
284
- })
537
+ }
285
538
  ```
286
539
 
287
- *NOTE: The expression DOM.select('.name').events('input') results in an observable that 'fires' or 'emits' whenever the DOM 'input' event occurs*
288
540
 
541
+ ## Common Tasks Made Easy
289
542
 
290
- ### Multiple Actions
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.
291
552
 
292
- Now let's improve the counter app with increment and decrement buttons as well as an input field to set the count to any value
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.
293
558
 
294
559
  ```javascript
295
- // app.jsx
296
- import { component } from 'sygnal'
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
+ }
297
567
 
298
- // import the xtream observable library so we can do some stream operations
299
- import xs from 'xstream'
300
-
301
- export default component({
302
- initialState: { count: 0 },
303
- model: {
304
- // add the value passed from the stream that triggered the action to the current count
305
- // this will either be 1 or -1, so will increment or decrement the count accordingly
306
- INCREMENT: (state, data) => ({ count: state.count + data }),
307
- SET_COUNT: (state, data) => ({ count: parseInt(data || 0) })
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>
579
+ }
580
+ ```
581
+ > NOTE: notice `collection` is lower case. This is to avoid problems with JSX conversion.
582
+
583
+
584
+ ### Switchable Components
585
+
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.
592
+
593
+ ```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, so the following will print something like:
627
+
628
+ ```javascript
629
+ {
630
+ 'first-name': 'First',
631
+ 'last-name': 'Last'
632
+ }
633
+ ```
634
+
635
+ ```javascript
636
+ import { processForm } from 'sygnal'
637
+
638
+ function MyComponent({ state }) {
639
+ return (
640
+ <form className="my-form">
641
+ <input name="first-name" />
642
+ <input name="last-name" />
643
+ <button type="submit">Submit</button>
644
+ </form>
645
+ )
646
+ }
647
+
648
+ MyComponent.intent = ({ DOM }) => {
649
+ const submit$ = processForm(DOM.select('.my-form'), { events: 'submit' })
650
+
651
+ return {
652
+ HANDLE_FORM: submit$
653
+ }
654
+ }
655
+
656
+ MyComponent.model = {
657
+ HANDLE_FORM: {
658
+ LOG: (state, data) => data
659
+ }
660
+ }
661
+ ```
662
+
663
+
664
+ ## Prerequisites / Using a Starter Template
665
+
666
+ For plain Javascript usage, Sygnal has no prerequisites as all dependencies are pre-bundled.
667
+
668
+ To bootstrap a minimal Sygnal application using Vite and that supports JSX:
669
+
670
+ ```bash
671
+ npx degit tpresley/sygnal-template my-awesome-app
672
+ cd my-awesome-app
673
+ npm install
674
+ npm run dev
675
+ ```
676
+
677
+ To build an optimized production ready version of your app:
678
+
679
+ ```bash
680
+ npm run build
681
+ ```
682
+
683
+ The results will be in the 'dist' folder, and you can serve it locally by running:
684
+
685
+ ```bash
686
+ npm preview
687
+ ```
688
+
689
+ 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:
690
+
691
+ ```javascript
692
+ // this example is for Vite or esbuild, but most bundlers have options similar to this for handling JSX transpiling
693
+ {
694
+ ...,
695
+ esbuild: {
696
+ // add the import for Sygnal's JSX and Fragment handler to the top of each .jsx and .tsx page automatically
697
+ jsxInject: `import { jsx, Fragment } from 'sygnal/jsx'`,
698
+ // tell the transpiler to use Sygnal's 'jsx' funtion to render JSX elements
699
+ jsxFactory: `jsx`,
700
+ // tell the transpiler to use Sygnal's 'Fragment' funtion to render JSX fragments (<>...</>)
701
+ jsxFragment: 'Fragment'
308
702
  },
309
- intent: ({ DOM }) => {
310
- // rather than pass streams directly to the actions, it is sometimes helpful
311
- // to collect them in variables first
312
- // it is convention (but not required) to name variables containing streams with a trailing '$'
313
- // the 'mapTo' function causes the stream to emit the specified value whenever the stream fires
314
- // so the increment$ stream will emit a '1' and the decrement$ stream a '-1' whenever their
315
- // respective buttons are pressed, and as usual those values will be passed to the 2nd parameter
316
- // of the reducer functions in the 'model'
317
- const increment$ = DOM.select('.increment').events('click').mapTo(1)
318
- const decrement$ = DOM.select('.decrement').events('click').mapTo(-1)
319
- const setCount$ = DOM.select('.number').events('input').map(e => e.target.value)
703
+ }
704
+ ```
320
705
 
321
- return {
322
- // the 'merge' function merges the events from all streams passed to it
323
- // this causes the 'INCREMENT' action to fire when either the increment$ or decrement$
324
- // streams fire, and will pass the value that the stream emeits (1 or -1 in this case)
325
- INCREMENT: xs.merge(increment$, decrement$),
326
- SET_COUNT: setCount$
706
+ 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:
707
+
708
+ ```javascript
709
+ {
710
+ ...,
711
+ build: {
712
+ minify: 'terser',
713
+ terserOptions: {
714
+ mangle: {
715
+ reserved: ['Fragment'],
716
+ },
327
717
  }
328
718
  },
329
- view: ({ state }) => {
330
- return (
331
- <div>
332
- <h1>Current Count: { state.count }</h1>
333
- <input type="button" className="increment" value="+" />
334
- <input type="button" className="decrement" value="-" />
335
- <input className="number" value={ state.count } />
336
- </div>
337
- )
719
+ }
720
+ ```
721
+
722
+ ## More Examples
723
+
724
+ ### Two Way Binding
725
+
726
+ ```javascript
727
+ function TwoWay({ state }) {
728
+ return (
729
+ <div>
730
+ <h1>Hello { state.name }</h1>
731
+ {/* set the 'value' of the input to the current state */}
732
+ <input className="name" value={ state.name } />
733
+ </div>
734
+ )
735
+ }
736
+
737
+ TwoWay.initialState = { name: 'World!' }
738
+
739
+ TwoWay.intent = ({ DOM }) => {
740
+ return {
741
+ // select the input DOM element using it's class name
742
+ // then map changes to the value ('input' event) to extract the value
743
+ // that value will then be passed to the 2nd parameter of reducers in 'model'
744
+ CHANGE_NAME: DOM.select('.name').events('input').map(e => e.target.value)
745
+ }
746
+ }
747
+
748
+ TwoWay.model = {
749
+ // update the name in the state whenever the 'CHANGE_NAME' action is triggered
750
+ // this time we use the 2nd parameter of the reducer function which gets the value passed
751
+ // by the stream that triggered the action
752
+ CHANGE_NAME: (state, data) => {
753
+ return { name: data }
754
+ }
755
+ }
756
+ ```
757
+
758
+
759
+ ### Multiple Actions
760
+
761
+ ```javascript
762
+ // import the xtream observable library so we can do some stream operations
763
+ import { xs } from 'sygnal'
764
+
765
+ function Counter({ state }) {
766
+ return (
767
+ <div>
768
+ <h1>Current Count: { state.count }</h1>
769
+ <input type="button" className="increment" value="+" />
770
+ <input type="button" className="decrement" value="-" />
771
+ <input className="number" value={ state.count } />
772
+ </div>
773
+ )
774
+ }
775
+
776
+ Counter.initialState = { count: 0 }
777
+
778
+ Counter.intent = ({ DOM }) => {
779
+ // rather than pass Observables directly to the actions, it is sometimes helpful
780
+ // to collect them in variables first
781
+ // it is convention (but not required) to name variables containing Observables with a trailing '$'
782
+ // the 'mapTo' function causes the Observable to emit the specified value whenever the stream fires
783
+ // so the increment$ stream will emit a '1' and the decrement$ stream a '-1' whenever their
784
+ // respective buttons are pressed, and as usual those values will be passed to the 2nd parameter
785
+ // of the reducer functions in the 'model'
786
+ const increment$ = DOM.select('.increment').events('click').mapTo(1)
787
+ const decrement$ = DOM.select('.decrement').events('click').mapTo(-1)
788
+ const setCount$ = DOM.select('.number').events('input').map(e => e.target.value)
789
+
790
+ return {
791
+ // the 'merge' function merges the events from all streams passed to it
792
+ // this causes the 'INCREMENT' action to fire when either the increment$ or decrement$
793
+ // streams fire, and will pass the value that the stream emeits (1 or -1 in this case)
794
+ INCREMENT: xs.merge(increment$, decrement$),
795
+ SET_COUNT: setCount$
338
796
  }
339
- })
797
+ }
798
+
799
+ Counter.model = {
800
+ // add the value passed from the stream that triggered the action to the current count
801
+ // this will either be 1 or -1, so will increment or decrement the count accordingly
802
+ INCREMENT: (state, data) => ({ count: state.count + data }),
803
+ SET_COUNT: (state, data) => ({ count: parseInt(data || 0) })
804
+ }
340
805
  ```
341
806
 
342
807
 
343
808
 
344
809
  ## More Documentation To Come...
345
810
 
346
- 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, and brings more attention to the wonderful work of the Cycle.js team.
811
+ 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.
347
812
 
348
813
  Until better documentation is available, here are some well-commented projects using most of Sygnal's features:
349
814
  - 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