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.
Files changed (3) hide show
  1. package/README.md +64 -76
  2. package/package.json +2 -2
  3. 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
- WIP.
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
- import { Data, Effect } from "effect";
17
- import { Effer, html, makeReducer } from "effer";
18
-
19
- type Msg = Data.TaggedEnum<{
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 _Counter = () => Effect.gen(function*() {
38
- const { attach, queueMsg } = yield* Effer
39
- const [counterStream, counterQueue] = yield* CounterService
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=${queueMsg(counterQueue, () => Increment())}
35
+ @click=${() => count.update(n => n - 1)}
47
36
  >
48
37
  - 1
49
38
  </button>
50
- Count is ${yield* attach(counterStream)}
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=${queueMsg(counterQueue, () => Decrement())}
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
- const ComponentsLayer = Layer.empty.pipe(
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 using events:
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 the stream of values as well as a queue to send updates to:
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
- // counterQueue will accept either Increment or Decrement and the stream will update
161
- // according to the update function
162
- const [counterStream, counterQueue] = yield* CounterService
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 the queue will accept new state values instead of messages.
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
- // the counterQueue now accepts a number (the new count) instead of update messages
180
- const [counterStream, counterQueue] = yield* CounterService
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effer",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "An Effect native UI library based on Lit-HTML",
@@ -74,4 +74,4 @@
74
74
  ]
75
75
  }
76
76
  }
77
- }
77
+ }
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 A tuple of the stream of the current state value and a queue to dispatch update messages
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 [ countStream, countQueue ] = yield* makeReducer(0, counterReducer)
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*() {