sygnal 1.0.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/.babelrc ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "presets": ["@babel/preset-env"]
3
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 tpresley
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,301 @@
1
+ # Sygnal
2
+
3
+ Simple library for making clean Cycle.js components
4
+
5
+
6
+ ## Cycle.js
7
+
8
+ Cycle.js is a functional reactive coding framework that asks 'what if the user was a function?'
9
+
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.
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.
15
+
16
+
17
+ ## Why?
18
+
19
+ Cycle.js is a powerful and criminally underappreciated framewaork that despite its many advantages (like fully isolated side-effects, functional reactive style, pure by nature components, extremely small number of dependencies and bumdle 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 sometimes complex stream plumbing that is required to layer and connect components.
20
+
21
+ Sygnal provides a structured way to create Cycle.js components that accomplishes several key goals:
22
+ - Minimize boilerplate
23
+ - Provide a simplified way to handle common application tasks
24
+ - Handle all stream plumbing between components
25
+ - Support arbitrarily complex applications with deep component hierarchies
26
+ - Reuse the best patterns from popular frameworks like React and Vue while avoiding the pitfalls
27
+ - Support pure Javascript, Typescript, and JSX
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
31
+ - Work with modern bundlers like Vite, and provide easy application bootstrapping
32
+
33
+
34
+ ## Features
35
+
36
+ Sygnal provides the following features for quickly building powerful components for building either a full Sygnal based application, or to be used in combination with existing Cycle.js components.
37
+
38
+ ### The component() Function
39
+
40
+ Sygnal's component() function is the only thing needed ot create single components. 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 it returns a function that accepts Cycle.js 'sources' and returns Cycle.js 'sinks').
41
+
42
+ The 3 most 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 from snabbdom or by using JSX transpiling using snabbdom-pragma)
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.
48
+
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 whould 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.
50
+
51
+ This strict separation of component logic makes reasoning about how to build the component easier, and makes refactoring and enhancing components a breeze.
52
+
53
+
54
+ ### The collection() Function
55
+
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.
57
+
58
+
59
+ ### The switchable() Function
60
+
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).
62
+
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.
64
+
65
+
66
+ ### The run() Function
67
+
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
+
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.*
76
+
77
+
78
+ ### The processForm() function
79
+
80
+ A very common task in web pages and borwser applications is to work with 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.
81
+
82
+
83
+
84
+ ## Prerequisites
85
+
86
+ The only prerequisites to use Sygnal are Cycle.js itself and either @cycle/dom or snabbdom-pragma for virtual dom rendering. However, to handle more advanced observable/stream operations, it's helpful to install [xstream](https://github.com/staltz/xstream "xstream reactive stream libraray), an observable library similar to Most or RxJS that is extremely small and fast, and is tailored for use in browser based functional reactive applications.
87
+
88
+ To bootstrap a minimal Sygnal application using Vite and JSX:
89
+
90
+ ```bash
91
+ npx degit tpresley/sygnal-template my-awesome-app
92
+ cd my-awesome-app
93
+ npm install
94
+ npm run dev
95
+ ```
96
+
97
+ To build an optimized production ready version:
98
+
99
+ ```bash
100
+ npm run build
101
+ ```
102
+
103
+ The results will be in the 'dist' folder, and you can serve it locally by running:
104
+
105
+ ```bash
106
+ npm preview
107
+ ```
108
+
109
+ Alternatively, you can use any other bundler of your choice (Webpack, babel, rollup, etc.). In order to generate the required virtual DOM in your 'view' function, you will either need to include [@cycle/dom](https://cycle.js.org/api/dom.html#api-h) to get access to virtual DOM helper functions, **or** you will need to install [snabbdom-pragma](https://www.npmjs.com/package/snabbdom-pragma) and configure your bundler to use it for transforming JSX (see [examples here](https://www.npmjs.com/package/snabbdom-pragma#usage "snabbdom-pragma bundler configuration examples)).
110
+
111
+
112
+ ## Initialization
113
+
114
+ 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):
115
+
116
+ ```javascript
117
+ import { run } from 'sygnal'
118
+ // replace the following line with your app's root component
119
+ import App from './app'
120
+
121
+ run(App) // <-- automatically binds to a #root HTML element (make sure you have an element with id="root" or the app won't start)
122
+ ```
123
+
124
+ 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:
125
+
126
+ ```bash
127
+ npm run dev
128
+ ```
129
+
130
+
131
+ ## Basic Examples
132
+
133
+ ### Hello World
134
+
135
+ The most basic (and not very useful) component
136
+
137
+ ```javascript
138
+ import { component } from 'sygnal'
139
+
140
+ export default component({
141
+ view: () => <h1>Hello World!</h1>
142
+ })
143
+ ```
144
+
145
+
146
+ ### Using state (basic)
147
+
148
+ 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.
149
+
150
+ This can be provided using the 'initialState' parameter of component().
151
+
152
+ ```javascript
153
+ import { component } from 'sygnal'
154
+
155
+ export default component({
156
+ initialState: { who: 'World!' },
157
+ view: ({ state }) => <h1>Hello { state.who }</h1>
158
+ // if you prefer not to use JSX, the above is equivalent to:
159
+ // view: ({ state }) => h('h2', `Hello ${ state.who }`)
160
+ // but you will need to add "import { h } from @cycle/dom" to the top of your file
161
+ })
162
+ ```
163
+
164
+ 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.
165
+
166
+
167
+ ### DOM Events
168
+
169
+ To make components capable of responding to users interacting with the DOM, you will need to add the 'model' and 'intent' parameters.
170
+
171
+ The 'model' parameter is an object that maps 'action' names to what should be done when that action happens.
172
+
173
+ 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.
174
+
175
+ This sounds more complicated than it is... basically the 'model' answers **what** can/should happen, and the 'intent' answers **when** those things will happen.
176
+
177
+ To illustrate, here's a basic counter that increments when the user clicks anywhere in the page:
178
+
179
+ ```javascript
180
+ import { component } from 'sygnal'
181
+
182
+ export default component({
183
+ // initialize the count to 0
184
+ initialState: { count: 0 },
185
+ model: {
186
+ // when the 'INCREMENT' action happens, run this 'reducer' function
187
+ // which takes the current state and returns the updated state,
188
+ // in this case incrementing the count by 1
189
+ INCREMENT: (state) => {
190
+ return { count: state.count + 1 }
191
+ }
192
+ },
193
+ // the 'sources' passed to intent() is an object containing an entry for each Cycle.js 'driver' passed to run() in index.js
194
+ // the DOM source allows you to select DOM elements by any valid CSS selector, and listen for any DOM events
195
+ // because we map document click events to the 'INCREMENT' action, it will cause the 'INCREMENT' action in 'model' to fire
196
+ // whenever the document is clicked
197
+ intent: (sources) => {
198
+ return {
199
+ INCREMENT: sources.DOM.select('document').events('click')
200
+ }
201
+ },
202
+ // every time the state is changed, the view will automatically be efficiently rerendered (only DOM elements that have changed will be impacted)
203
+ view: ({ state }) => <h1>Current Count: { state.count }</h1>
204
+ })
205
+ ```
206
+
207
+ *NOTE: action names (like INCREMENT in the above example) can be any valid Javascript object key name*
208
+
209
+
210
+ ### DOM Events (part 2)
211
+
212
+ Now let's improve our Hello World app with 2-way binding on an input field
213
+
214
+ ```javascript
215
+ import { component } from 'sygnal'
216
+
217
+ export default component({
218
+ // initial name
219
+ initialState: { name: 'World!' },
220
+ model: {
221
+ // update the name in the state whenever the 'CHANGE_NAME' action is triggered
222
+ // this time we use the 2nd parameter of the reducer function which gets the value passed
223
+ // by the stream that triggered the action
224
+ CHANGE_NAME: (state, data) => {
225
+ return { name: data }
226
+ }
227
+ },
228
+ // it's usually more convenient to use destructuring to 'get' the individual sources you need, like DOM in this case
229
+ intent: ({ DOM }) => {
230
+ return {
231
+ // select the input DOM element using it's class name
232
+ // then map changes to the value ('input' event) to extract the value
233
+ // that value will then be passed to the 2nd parameter of reducers in 'model'
234
+ CHANGE_NAME: DOM.select('.name').events('input').map(e => e.target.value)
235
+ }
236
+ },
237
+ view: ({ state }) => {
238
+ return (
239
+ <div>
240
+ <h1>Hello { state.name }</h1>
241
+ {/* set the 'value' of the input to the current state */}
242
+ <input className="name" value={ state.name } />
243
+ </div>
244
+ )
245
+ }
246
+ })
247
+ ```
248
+
249
+ *NOTE: The expression DOM.select('.name').events('input') results in an observable that 'fires' or 'emits' whenever the DOM 'input' event occurs*
250
+
251
+
252
+ ### Multiple Actions
253
+
254
+ 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
255
+
256
+ ```javascript
257
+ import { component } from 'sygnal'
258
+
259
+ // import the xtream observable library so we can do some stream operations
260
+ import xs from 'xstream'
261
+
262
+ export default component({
263
+ initialState: { count: 0 },
264
+ model: {
265
+ // add the value passed from the stream that triggered the action to the current count
266
+ // this will either be 1 or -1, so will increment or decrement the count accordingly
267
+ INCREMENT: (state, data) => ({ count: state.count + data }),
268
+ SET_COUNT: (state, data) => ({ count: parseInt(data || 0) })
269
+ },
270
+ intent: ({ DOM }) => {
271
+ // rather than pass streams directly to the actions, it is sometimes helpful
272
+ // to collect them in variables first
273
+ // it is convention (but not required) to name variables containing streams with a trailing '$'
274
+ // the 'mapTo' function causes the stream to emit the specified value whenever the stream fires
275
+ // so the increment$ stream will emit a '1' and the decrement$ stream a '-1' whenever their
276
+ // respective buttons are pressed, and as usual those values will be passed to the 2nd parameter
277
+ // of the reducer functions in the 'model'
278
+ const increment$ = DOM.select('.increment').events('click').mapTo(1)
279
+ const decrement$ = DOM.select('.decrement').events('click').mapTo(-1)
280
+ const setCount$ = DOM.select('.number').events('input').map(e => e.target.value)
281
+
282
+ return {
283
+ // the 'merge' function merges the events from all streams passed to it
284
+ // this causes the 'INCREMENT' action to fire when either the increment$ or decrement$
285
+ // streams fire, and will pass the value that the stream emeits (1 or -1 in this case)
286
+ INCREMENT: xs.merge(increment$, decrement$),
287
+ SET_COUNT: setCount$
288
+ }
289
+ },
290
+ view: ({ state }) => {
291
+ return (
292
+ <div>
293
+ <h1>Current Count: { state.count }</h1>
294
+ <input type="button" className="increment" value="+" />
295
+ <input type="button" className="decrement" value="-" />
296
+ <input className="number" value={ state.count } />
297
+ </div>
298
+ )
299
+ }
300
+ })
301
+ ```
@@ -0,0 +1,83 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports["default"] = collection;
7
+
8
+ var _isolate = _interopRequireDefault(require("@cycle/isolate"));
9
+
10
+ var _state = require("@cycle/state");
11
+
12
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
13
+
14
+ function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
15
+
16
+ function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }
17
+
18
+ function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
19
+
20
+ function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
21
+
22
+ function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
23
+
24
+ function _iterableToArrayLimit(arr, i) { var _i = arr == null ? null : typeof Symbol !== "undefined" && arr[Symbol.iterator] || arr["@@iterator"]; if (_i == null) return; var _arr = []; var _n = true; var _d = false; var _s, _e; try { for (_i = _i.call(arr); !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }
25
+
26
+ function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
27
+
28
+ function collection(component, stateLense) {
29
+ var combineList = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : ['DOM'];
30
+ var globalList = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : ['EVENTS'];
31
+ var stateSourceName = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 'STATE';
32
+ return function (sources) {
33
+ var collectionOpts = {
34
+ item: component,
35
+ itemKey: function itemKey(state) {
36
+ return state.id;
37
+ },
38
+ itemScope: function itemScope(key) {
39
+ return key;
40
+ },
41
+ channel: stateSourceName,
42
+ collectSinks: function collectSinks(instances) {
43
+ return Object.entries(sources).reduce(function (acc, _ref) {
44
+ var _ref2 = _slicedToArray(_ref, 2),
45
+ name = _ref2[0],
46
+ stream = _ref2[1];
47
+
48
+ if (combineList.includes(name)) {
49
+ acc[name] = instances.pickCombine(name);
50
+ } else {
51
+ acc[name] = instances.pickMerge(name);
52
+ }
53
+
54
+ return acc;
55
+ }, {});
56
+ }
57
+ };
58
+
59
+ var isolateOpts = _defineProperty({}, stateSourceName, stateLense);
60
+
61
+ globalList.forEach(function (global) {
62
+ return isolateOpts[global] = null;
63
+ });
64
+ combineList.forEach(function (combine) {
65
+ return isolateOpts[combine] = null;
66
+ });
67
+ return makeIsolatedCollection(collectionOpts, isolateOpts, sources);
68
+ };
69
+ }
70
+ /**
71
+ * instantiate a cycle collection and isolate
72
+ * (makes the code for doing isolated collections more readable)
73
+ *
74
+ * @param {Object} collectionOpts options for the makeCollection function (see cycle/state documentation)
75
+ * @param {String|Object} isolateOpts options for the isolate function (see cycle/isolate documentation)
76
+ * @param {Object} sources object of cycle style sources to use for the created collection
77
+ * @return {Object} collection of component sinks
78
+ */
79
+
80
+
81
+ function makeIsolatedCollection(collectionOpts, isolateOpts, sources) {
82
+ return (0, _isolate["default"])((0, _state.makeCollection)(collectionOpts), isolateOpts)(sources);
83
+ }