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 +110 -710
- package/dist/astro/client.cjs.js +4870 -0
- package/dist/astro/client.esm.js +4665 -0
- package/dist/astro/client.mjs +4849 -0
- package/dist/astro/index.cjs.js +29 -0
- package/dist/astro/index.esm.js +27 -0
- package/dist/astro/index.mjs +27 -0
- package/dist/astro/server.cjs.js +38 -0
- package/dist/astro/server.esm.js +25 -0
- package/dist/astro/server.mjs +31 -0
- package/dist/index.cjs.js +519 -66
- package/dist/index.d.ts +294 -93
- package/dist/index.esm.js +489 -60
- package/dist/jsx.cjs.js +23 -1
- package/dist/jsx.esm.js +23 -1
- package/dist/sygnal.min.js +1 -1
- package/package.json +19 -2
- package/src/astro/client.d.ts +8 -0
- package/src/astro/client.js +47 -0
- package/src/astro/index.d.ts +15 -0
- package/src/astro/index.js +25 -0
- package/src/astro/server.d.ts +9 -0
- package/src/astro/server.js +29 -0
- package/src/component.js +32 -8
- package/src/extra/dragDriver.js +174 -0
- package/src/extra/driverFactories.js +2 -2
- package/src/extra/eventDriver.js +1 -1
- package/src/extra/exactState.js +5 -0
- package/src/extra/hmr.js +50 -0
- package/src/extra/processDrag.js +25 -0
- package/src/extra/processForm.js +2 -2
- package/src/extra/run.js +99 -17
- package/src/extra/xstreamCompat.js +41 -0
- package/src/index.d.ts +358 -97
- package/src/index.js +5 -1
- package/src/pragma/index.js +23 -1
- package/src/switchable.js +5 -3
- package/src/sygnal.d.ts +4 -47
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
153
|
-
|
|
36
|
+
## In a Nutshell
|
|
154
37
|
|
|
155
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
173
|
-
<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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
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
|
-
|
|
260
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
95
|
+
### Collections
|
|
372
96
|
|
|
373
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
468
|
-
return {
|
|
469
|
-
INCREMENT: DOM.select('.increment-button').events('click')
|
|
470
|
-
}
|
|
471
|
-
}
|
|
105
|
+
Swap between components based on state:
|
|
472
106
|
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
|
|
111
|
+
### Form Handling
|
|
483
112
|
|
|
484
|
-
|
|
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
|
-
|
|
489
|
-
|
|
115
|
+
```jsx
|
|
116
|
+
import { processForm } from 'sygnal'
|
|
490
117
|
|
|
491
|
-
|
|
492
|
-
DOM.select('.
|
|
118
|
+
MyForm.intent = ({ DOM }) => ({
|
|
119
|
+
SUBMITTED: processForm(DOM.select('.my-form'), { events: 'submit' })
|
|
120
|
+
})
|
|
493
121
|
```
|
|
494
122
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
### Do Multiple Things For One Action
|
|
123
|
+
### Custom Drivers
|
|
499
124
|
|
|
500
|
-
|
|
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
|
-
|
|
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
|
-
|
|
518
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
-
|
|
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
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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
|
-
```
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
-
|
|
168
|
+
Full type definitions included:
|
|
666
169
|
|
|
667
|
-
|
|
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
|
-
|
|
173
|
+
type AppState = { count: number }
|
|
174
|
+
type AppActions = { INCREMENT: null }
|
|
679
175
|
|
|
680
|
-
|
|
681
|
-
|
|
176
|
+
const App: RootComponent<AppState, {}, AppActions> = ({ state }) => (
|
|
177
|
+
<div>{state.count}</div>
|
|
178
|
+
)
|
|
682
179
|
```
|
|
683
180
|
|
|
684
|
-
|
|
181
|
+
## Bundler Setup (JSX)
|
|
685
182
|
|
|
686
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
764
|
-
|
|
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
|
-
|
|
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
|
-
|
|
215
|
+
## License
|
|
813
216
|
|
|
814
|
-
|
|
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
|