effer 0.0.1 → 0.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 +64 -76
- package/package.json +2 -2
- package/src/index.ts +2 -2
package/README.md
CHANGED
|
@@ -4,7 +4,9 @@ An Effect native UI library based on lit-html.
|
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
```
|
|
8
|
+
npm install effer
|
|
9
|
+
```
|
|
8
10
|
|
|
9
11
|
## Creating a Counter with Effer
|
|
10
12
|
|
|
@@ -12,46 +14,33 @@ Example:
|
|
|
12
14
|
|
|
13
15
|
```ts
|
|
14
16
|
// INSIDE counter.ts
|
|
17
|
+
import { Effect } from "effect";
|
|
18
|
+
import { Effer, NavService, html, makeState } from "effer";
|
|
15
19
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
Increment: {};
|
|
21
|
-
Decrement: {};
|
|
22
|
-
}>
|
|
23
|
-
const { Increment, Decrement, $match } = Data.taggedEnum<Msg>()
|
|
24
|
-
|
|
25
|
-
const makeCounterState = makeReducer(
|
|
26
|
-
0,
|
|
27
|
-
(state: number, msg: Msg) => $match({
|
|
28
|
-
Increment: () => Effect.succeed(state + 1),
|
|
29
|
-
Decrement: () => Effect.succeed(state - 1),
|
|
30
|
-
})(msg)
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
export class CounterService extends Effect.Service<CounterService>()('CounterService', {
|
|
34
|
-
effect: makeCounterState
|
|
20
|
+
// Our business logic is created seperately from our views
|
|
21
|
+
// This makes it easy to mock or modify the behavior of a component
|
|
22
|
+
export class CountState extends Effect.Service<CountState>()("CountState", {
|
|
23
|
+
effect: makeState<number>(0)
|
|
35
24
|
}) {}
|
|
36
25
|
|
|
37
|
-
const
|
|
38
|
-
const { attach
|
|
39
|
-
const
|
|
26
|
+
export const Counter = () => Effect.gen(function*() {
|
|
27
|
+
const { attach } = yield* Effer
|
|
28
|
+
const count = yield* CountState
|
|
40
29
|
|
|
41
30
|
return html`
|
|
42
31
|
<section class="counter-container d-flex flex-row align-items-baseline justify-content-center">
|
|
43
32
|
<button
|
|
44
33
|
class="btn btn-primary btn-sm counter-button"
|
|
45
34
|
id="decButton"
|
|
46
|
-
@click=${
|
|
35
|
+
@click=${() => count.update(n => n - 1)}
|
|
47
36
|
>
|
|
48
37
|
- 1
|
|
49
38
|
</button>
|
|
50
|
-
Count is ${yield* attach(
|
|
39
|
+
Count is ${yield* attach(count.stream)}
|
|
51
40
|
<button
|
|
52
41
|
class="btn btn-primary btn-sm counter-button"
|
|
53
42
|
id="incButton"
|
|
54
|
-
@click=${
|
|
43
|
+
@click=${() => count.update(n => n + 1)}
|
|
55
44
|
>
|
|
56
45
|
+ 1
|
|
57
46
|
</button>
|
|
@@ -59,43 +48,29 @@ const _Counter = () => Effect.gen(function*() {
|
|
|
59
48
|
`
|
|
60
49
|
})
|
|
61
50
|
|
|
62
|
-
// Optional - make your component a service for ease of dependency management and mocking
|
|
63
|
-
|
|
64
|
-
export class Counter extends Effect.Service<Counter>()('Counter', {
|
|
65
|
-
effect: _Counter(),
|
|
66
|
-
dependencies: [CounterService.Default, Effer.Default]
|
|
67
|
-
}) {}
|
|
68
|
-
|
|
69
51
|
// INSIDE main.ts
|
|
52
|
+
import { BrowserRuntime } from "@effect/platform-browser";
|
|
53
|
+
import { Effect, Layer } from "effect";
|
|
54
|
+
import { layer, render } from "effer";
|
|
55
|
+
import { Counter } from "./counter";
|
|
70
56
|
|
|
71
|
-
|
|
72
|
-
Layer.merge(Counter.Default),
|
|
73
|
-
// ... merge in any other component layers
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
App().pipe(
|
|
57
|
+
Counter().pipe(
|
|
77
58
|
Effect.andThen(
|
|
78
59
|
// render our component to the DOM element with ID 'app'
|
|
79
60
|
app => render(app, document.getElementById('app')!)
|
|
80
61
|
),
|
|
81
62
|
Layer.effectDiscard, // transform the render effect into a Layer
|
|
82
|
-
Layer.provide(ComponentsLayer), // merge in Component layers
|
|
83
63
|
Layer.provideMerge(layer), // merge in Effer layer
|
|
84
64
|
Layer.launch, // launch our combined Effer app layer
|
|
85
65
|
BrowserRuntime.runMain
|
|
86
66
|
)
|
|
87
67
|
```
|
|
88
68
|
|
|
89
|
-
|
|
90
|
-
To bring our UI and business logic together, Effer gives us two tools: attach and queueMsg, both available in the Effer service.
|
|
91
|
-
|
|
92
|
-
```ts
|
|
93
|
-
const { attach, queueMsg } = yield* Effer
|
|
94
|
-
```
|
|
95
|
-
|
|
96
69
|
In the UI, we need to represent data changing over time. Effect gives us Streams as a way to do that. The attach method of the Effer service lets you "attach" a stream (or anything that can be converted to a Stream) to the UI, and the UI will automatically display the most up-to-date value of that Stream.
|
|
97
70
|
|
|
98
71
|
```ts
|
|
72
|
+
const { attach } = yield* Effer
|
|
73
|
+
|
|
99
74
|
html`
|
|
100
75
|
Count is ${yield* attach(counterStream)}
|
|
101
76
|
`
|
|
@@ -113,25 +88,13 @@ type Attachable<A,E,R> =
|
|
|
113
88
|
| Channel<Chunk.Chunk<A>, unknown, E, unknown, unknown, unknown, R>
|
|
114
89
|
```
|
|
115
90
|
|
|
116
|
-
When we need to handle events from the UI, we do so with the queueMsg method of the Effer service. This function can go anywhere an event listener callback is expected (like onclick, onchange, etc.). It takes a queue to send the event to, and a function that maps from the DOM event to the queue's event that we want to dispatch:
|
|
117
|
-
|
|
118
|
-
```ts
|
|
119
|
-
html`
|
|
120
|
-
<button
|
|
121
|
-
class="btn btn-primary btn-sm counter-button"
|
|
122
|
-
id="decButton"
|
|
123
|
-
@click=${queueMsg(counterQueue, (e: MouseEvent) => Increment())}
|
|
124
|
-
>
|
|
125
|
-
`
|
|
126
|
-
```
|
|
127
|
-
|
|
128
91
|
## Managing State
|
|
129
92
|
|
|
130
93
|
### makeReducer - For Complex State or Logic
|
|
131
94
|
|
|
132
95
|
To manage state, Effer provides two helper functions: makeReducer and makeState. These are very similar to useReducer and useState in React, except you use them outside your UI components as part of your business logic.
|
|
133
96
|
|
|
134
|
-
makeReducer lets you define a state and update it
|
|
97
|
+
makeReducer lets you define a state and update it by dispatching messages:
|
|
135
98
|
|
|
136
99
|
```ts
|
|
137
100
|
type Msg = Data.TaggedEnum<{
|
|
@@ -154,30 +117,25 @@ export class CounterService extends Effect.Service<CounterService>()('CounterSer
|
|
|
154
117
|
}) {}
|
|
155
118
|
```
|
|
156
119
|
|
|
157
|
-
We define the expected update messages as Increment and Decrement, and we provide a function that takes the old state and a message, and provides a new updated state. Now when we inject the service, we will get
|
|
120
|
+
We define the expected update messages as Increment and Decrement, and we provide a function that takes the old state and a message, and provides a new updated state. Now when we inject the service, we will get an object with this interface:
|
|
158
121
|
|
|
159
122
|
```ts
|
|
160
|
-
|
|
161
|
-
//
|
|
162
|
-
|
|
123
|
+
{
|
|
124
|
+
stream: Stream<A> // where A represents our state type
|
|
125
|
+
dispatch: (msg: M) => boolean // where M represents the type of message we can use to update our state
|
|
126
|
+
}
|
|
163
127
|
```
|
|
164
128
|
|
|
165
129
|
### makeState - For Simple State Management
|
|
166
130
|
|
|
167
|
-
makeState is very similar, but more simplified. We just provide an initial state, and
|
|
168
|
-
|
|
169
|
-
```ts
|
|
170
|
-
const makeCounterState = makeState(0)
|
|
171
|
-
|
|
172
|
-
// Making our state values a Service so we can easily pass it around
|
|
173
|
-
export class CounterService extends Effect.Service<CounterService>()('CounterService', {
|
|
174
|
-
effect: makeCounterState
|
|
175
|
-
}) {}
|
|
176
|
-
```
|
|
131
|
+
makeState is very similar, but more simplified. We just provide an initial state, and we get an object with this interface:
|
|
177
132
|
|
|
178
133
|
```ts
|
|
179
|
-
|
|
180
|
-
|
|
134
|
+
{
|
|
135
|
+
stream: Stream<A> // where A represents our state type
|
|
136
|
+
set: (val: A) => boolean // replace the current value with a new value
|
|
137
|
+
update: (updateFn: (oldValue: A) => A) => boolean // update with a function to access old value
|
|
138
|
+
}
|
|
181
139
|
```
|
|
182
140
|
|
|
183
141
|
### makeAsyncResult - For Async State and Logic
|
|
@@ -214,6 +172,22 @@ const postsTable = posts.stream.pipe(
|
|
|
214
172
|
|
|
215
173
|
Note that since `Result<A,E>` is a TaggedEnum, `match()` is exactly the same as the `$match()` function you get from creating a TaggedEnum. See the [Effect Documentation](https://effect.website/docs/data-types/data/#union-of-tagged-structs)
|
|
216
174
|
|
|
175
|
+
## Logic Outside of State Flows
|
|
176
|
+
|
|
177
|
+
If you have logic that doesn't fit into a state or reducer update, Effer offers the queueMessage function. This function can go anywhere an event listener callback is expected (like onclick, onchange, etc.). It takes a queue to send the event to, and a function that maps from the DOM event to the queue's value type that we want to dispatch:
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
html`
|
|
181
|
+
<button
|
|
182
|
+
class="btn btn-primary btn-sm counter-button"
|
|
183
|
+
id="decButton"
|
|
184
|
+
@click=${queueMsg(counterQueue, (e: MouseEvent) => Increment())}
|
|
185
|
+
>
|
|
186
|
+
`
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
You can then take that queue (in this case, counterQueue) and run it into a Stream, working with the events as you need to.
|
|
190
|
+
|
|
217
191
|
## Navigation and URLs
|
|
218
192
|
|
|
219
193
|
Effer offers a NavService for handling things related to navigation and URLs. Here is its interface:
|
|
@@ -272,3 +246,17 @@ const startingCount: number = yield* getQueryParam('count').pipe(
|
|
|
272
246
|
)
|
|
273
247
|
```
|
|
274
248
|
|
|
249
|
+
## Live Examples
|
|
250
|
+
|
|
251
|
+
You can see example code and play around with Effer by forking this CodeSandbox [HERE](https://codesandbox.io/p/devbox/effer-examples-cj8lcy)
|
|
252
|
+
|
|
253
|
+
Where to find specific examples:
|
|
254
|
+
- the `attach()` function: All of the Effer components in the sandboxs use attach
|
|
255
|
+
- the `makeState()` function: counter.ts
|
|
256
|
+
- the `makeReducer()` function: todo.controller.ts
|
|
257
|
+
- the `makeAsyncResult()` function: posts.api.ts and posts.view.ts
|
|
258
|
+
- handling navigation: app.ts
|
|
259
|
+
- getting a query param: counter.ts
|
|
260
|
+
- using browser storage: todo.controller.ts
|
|
261
|
+
- making HTTP requests/working with Effect clients: posts.api.ts and posts.view.ts
|
|
262
|
+
- working with third party libraries like AG Grid: todo.controller.ts and todo.view.ts
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -111,7 +111,7 @@ export class Effer extends Effect.Service<Effer>()('Effer', {
|
|
|
111
111
|
* Creates a stream of the latest state value, and a queue to update the value.
|
|
112
112
|
* @param initialState The starting state value
|
|
113
113
|
* @param updateFn An effectful function that takes the old state, an update message, and returns a new state
|
|
114
|
-
* @returns
|
|
114
|
+
* @returns An object with a stream field representing a stream of the current state value and a dispatch method for dispatching update messages
|
|
115
115
|
*
|
|
116
116
|
* ```ts
|
|
117
117
|
* type Msg = Data.TaggedEnum<{
|
|
@@ -125,7 +125,7 @@ export class Effer extends Effect.Service<Effer>()('Effer', {
|
|
|
125
125
|
* })
|
|
126
126
|
*
|
|
127
127
|
* // Inside an Effect
|
|
128
|
-
* const
|
|
128
|
+
* const {stream, dispatch} = yield* makeReducer(0, counterReducer)
|
|
129
129
|
* ```
|
|
130
130
|
*/
|
|
131
131
|
export const makeReducer = <A,M,E=never,R=never>(initialState: A, updateFn: (state: A, msg: M) => Effect.Effect<A,E,R>) => Effect.gen(function*() {
|