@jay-framework/component 0.5.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.
@@ -0,0 +1,69 @@
1
+ import { JayElement, ContextMarker, PreRenderElement, JayComponent, EventEmitter } from '@jay-framework/runtime';
2
+ import { Getter, Reactive, ValueOrGetter, Setter } from '@jay-framework/reactive';
3
+ import { JSONPatch } from '@jay-framework/json-patch';
4
+ import { HTMLElement } from 'node-html-parser';
5
+
6
+ type Patcher<T> = (...patch: JSONPatch) => void;
7
+ type hasProps<PropsT> = {
8
+ props: Getter<PropsT>;
9
+ };
10
+ type Props<PropsT> = hasProps<PropsT> & {
11
+ [K in keyof PropsT]: K extends string ? Getter<PropsT[K]> : PropsT[K];
12
+ };
13
+ type ViewStateGetters<ViewState> = {
14
+ [K in keyof ViewState]: ViewState[K] | Getter<ViewState[K]>;
15
+ };
16
+ type UpdatableProps<PropsT> = Props<PropsT> & {
17
+ update(newProps: PropsT): any;
18
+ };
19
+ interface JayComponentCore<PropsT, ViewState> {
20
+ render: () => ViewStateGetters<ViewState>;
21
+ }
22
+ type ConcreteJayComponent1<PropsT extends object, ViewState, Refs, CompCore extends JayComponentCore<PropsT, ViewState>, JayElementT extends JayElement<ViewState, Refs>> = Omit<CompCore, 'render'> & JayComponent<PropsT, ViewState, JayElementT>;
23
+ type ConcreteJayComponent<PropsT extends object, ViewState, Refs, CompCore extends JayComponentCore<PropsT, ViewState>, JayElementT extends JayElement<ViewState, Refs>> = ConcreteJayComponent1<PropsT, ViewState, Refs, CompCore, JayElementT>;
24
+ declare function materializeViewState<ViewState extends object>(vsValueOrGetter: ViewStateGetters<ViewState>): ViewState;
25
+ type ComponentConstructor<PropsT extends object, Refs extends object, ViewState extends object, Contexts extends Array<any>, CompCore extends JayComponentCore<PropsT, ViewState>> = (props: Props<PropsT>, refs: Refs, ...contexts: Contexts) => CompCore;
26
+ type ContextMarkers<T extends any[]> = {
27
+ [K in keyof T]: ContextMarker<T[K]>;
28
+ };
29
+ declare function makeJayComponent<PropsT extends object, ViewState extends object, Refs extends object, JayElementT extends JayElement<ViewState, Refs>, Contexts extends Array<any>, CompCore extends JayComponentCore<PropsT, ViewState>>(preRender: PreRenderElement<ViewState, Refs, JayElementT>, comp: ComponentConstructor<PropsT, Refs, ViewState, Contexts, CompCore>, ...contextMarkers: ContextMarkers<Contexts>): (props: PropsT) => ConcreteJayComponent<PropsT, ViewState, Refs, CompCore, JayElementT>;
30
+ type JsxNode = HTMLElement;
31
+ type JayTsxComponentConstructor<PropsT extends object, CompT extends {
32
+ render: () => JsxNode;
33
+ }> = (props: Props<PropsT>) => CompT;
34
+ declare function makeJayTsxComponent<PropsT extends object, CompT extends {
35
+ render: () => JsxNode;
36
+ }>(comp: JayTsxComponentConstructor<PropsT, CompT>): JayComponent<PropsT, any, any>;
37
+ declare function makePropsProxy<PropsT extends object>(reactive: Reactive, props: PropsT): UpdatableProps<PropsT>;
38
+ declare const forTesting: {
39
+ makePropsProxy: typeof makePropsProxy;
40
+ };
41
+
42
+ declare const CONTEXT_REACTIVE_SYMBOL_CONTEXT: unique symbol;
43
+ declare function createReactiveContext<T extends object>(mkContext: () => T): T;
44
+
45
+ type EffectCleanup = () => void;
46
+ declare function createEffect(effect: () => void | EffectCleanup): void;
47
+ declare function createSignal<T>(value: ValueOrGetter<T>): [get: Getter<T>, set: Setter<T>];
48
+ declare function createPatchableSignal<T>(value: ValueOrGetter<T>): [get: Getter<T>, set: Setter<T>, patchFunc: Patcher<T>];
49
+ declare function useReactive(): Reactive;
50
+ declare function createMemo<T>(computation: (prev: T) => T, initialValue?: T): Getter<T>;
51
+ declare function createDerivedArray<T extends object, U>(arrayGetter: Getter<T[]>, mapCallback: (item: Getter<T>, index: Getter<number>, length: Getter<number>) => U): Getter<U[]>;
52
+ declare function createEvent<EventType>(eventEffect?: (emitter: EventEmitter<EventType, any>) => void): EventEmitter<EventType, any>;
53
+ declare function provideContext<ContextType>(marker: ContextMarker<ContextType>, context: ContextType): void;
54
+ declare function provideReactiveContext<ContextType extends object>(marker: ContextMarker<ContextType>, mkContext: () => ContextType): ContextType;
55
+
56
+ interface HookContext {
57
+ reactive: Reactive;
58
+ mountedSignal: [Getter<boolean>, Setter<boolean>];
59
+ provideContexts: [ContextMarker<any>, any][];
60
+ }
61
+ interface ComponentContext extends HookContext {
62
+ reactive: Reactive;
63
+ getComponentInstance: () => JayComponent<any, any, any>;
64
+ mountedSignal: [Getter<boolean>, Setter<boolean>];
65
+ }
66
+ declare const COMPONENT_CONTEXT: ContextMarker<ComponentContext>;
67
+ declare const CONTEXT_CREATION_CONTEXT: ContextMarker<HookContext>;
68
+
69
+ export { COMPONENT_CONTEXT, CONTEXT_CREATION_CONTEXT, CONTEXT_REACTIVE_SYMBOL_CONTEXT, type ComponentConstructor, type ComponentContext, type ConcreteJayComponent, type ContextMarkers, type EffectCleanup, type HookContext, type JayComponentCore, type JayTsxComponentConstructor, type JsxNode, type Patcher, type Props, type UpdatableProps, type ViewStateGetters, createDerivedArray, createEffect, createEvent, createMemo, createPatchableSignal, createReactiveContext, createSignal, forTesting, type hasProps, makeJayComponent, makeJayTsxComponent, makePropsProxy, materializeViewState, provideContext, provideReactiveContext, useReactive };
package/dist/index.js ADDED
@@ -0,0 +1,322 @@
1
+ import { createJayContext, withContext, findContext, useContext } from "@jay-framework/runtime";
2
+ import { mkReactive, GetterMark, SetterMark, MeasureOfChange } from "@jay-framework/reactive";
3
+ import { patch } from "@jay-framework/json-patch";
4
+ const COMPONENT_CONTEXT = createJayContext();
5
+ const CONTEXT_CREATION_CONTEXT = createJayContext();
6
+ const CONTEXT_REACTIVE_SYMBOL_CONTEXT = Symbol("context-reactive");
7
+ function newContextProxy(reactive, context) {
8
+ const wrapped = /* @__PURE__ */ new Map();
9
+ const wrap = (target) => {
10
+ if (!wrapped.has(target))
11
+ wrapped.set(
12
+ target,
13
+ (...args) => reactive.batchReactions(() => target.apply(context, args))
14
+ );
15
+ return wrapped.get(target);
16
+ };
17
+ return new Proxy(context, {
18
+ get(target, p, receiver) {
19
+ if (p === CONTEXT_REACTIVE_SYMBOL_CONTEXT)
20
+ return reactive;
21
+ else if (!target[p][GetterMark] && !target[p][SetterMark] && typeof target[p] === "function")
22
+ return wrap(target[p]);
23
+ else
24
+ return target[p];
25
+ }
26
+ });
27
+ }
28
+ function createReactiveContext(mkContext) {
29
+ const reactive = mkReactive("ctx", mkContext.name);
30
+ const context = withContext(
31
+ CONTEXT_CREATION_CONTEXT,
32
+ { reactive, mountedSignal: reactive.createSignal(true), provideContexts: [] },
33
+ mkContext
34
+ );
35
+ return newContextProxy(reactive, context);
36
+ }
37
+ function currentHookContext() {
38
+ return findContext((_) => _ === COMPONENT_CONTEXT || _ === CONTEXT_CREATION_CONTEXT);
39
+ }
40
+ function createEffect(effect) {
41
+ let cleanup = void 0;
42
+ const clean = () => {
43
+ if (cleanup !== void 0) {
44
+ cleanup();
45
+ cleanup = void 0;
46
+ }
47
+ };
48
+ let lastMounted = false;
49
+ const mounted = currentHookContext().mountedSignal[0];
50
+ currentHookContext().reactive.createReaction(() => {
51
+ if (lastMounted !== mounted()) {
52
+ if (mounted())
53
+ cleanup = effect();
54
+ else
55
+ clean();
56
+ lastMounted = mounted();
57
+ } else if (mounted()) {
58
+ clean();
59
+ cleanup = effect();
60
+ }
61
+ });
62
+ }
63
+ function createSignal(value) {
64
+ return currentHookContext().reactive.createSignal(value);
65
+ }
66
+ function createPatchableSignal(value) {
67
+ const [get, set] = createSignal(value);
68
+ const patchFunc = (...jsonPatch) => set(patch(get(), jsonPatch));
69
+ return [get, set, patchFunc];
70
+ }
71
+ function useReactive() {
72
+ return currentHookContext().reactive;
73
+ }
74
+ function createMemo(computation, initialValue) {
75
+ let [value, setValue] = currentHookContext().reactive.createSignal(initialValue);
76
+ currentHookContext().reactive.createReaction(() => {
77
+ setValue((oldValue) => computation(oldValue));
78
+ });
79
+ return value;
80
+ }
81
+ function trackableGetter(value) {
82
+ let wasUsed = false;
83
+ return {
84
+ getter: () => {
85
+ wasUsed = true;
86
+ return value;
87
+ },
88
+ wasUsed: () => wasUsed
89
+ };
90
+ }
91
+ function mapItem(item, index, length, force, cached, mapCallback) {
92
+ const itemGetter = trackableGetter(item);
93
+ const indexGetter = trackableGetter(index);
94
+ const lengthGetter = trackableGetter(length);
95
+ const needToMap = force || !cached || item !== cached.item || index !== cached.index && cached.usedIndex || length !== cached.length && cached.usedLength;
96
+ if (needToMap) {
97
+ const mappedItem = mapCallback(itemGetter.getter, indexGetter.getter, lengthGetter.getter);
98
+ return {
99
+ item,
100
+ mappedItem,
101
+ index,
102
+ length,
103
+ usedIndex: indexGetter.wasUsed(),
104
+ usedLength: lengthGetter.wasUsed()
105
+ };
106
+ } else
107
+ return cached;
108
+ }
109
+ function createDerivedArray(arrayGetter, mapCallback) {
110
+ let [sourceArray] = currentHookContext().reactive.createSignal(
111
+ arrayGetter,
112
+ MeasureOfChange.PARTIAL
113
+ );
114
+ let [mappedArray, setMappedArray] = createSignal([]);
115
+ let mappedItemsCache = /* @__PURE__ */ new WeakMap();
116
+ currentHookContext().reactive.createReaction((measureOfChange) => {
117
+ let newMappedItemsCache = /* @__PURE__ */ new WeakMap();
118
+ setMappedArray((oldValue) => {
119
+ let length = sourceArray().length;
120
+ let newMappedArray = sourceArray().map((item, index) => {
121
+ const force = measureOfChange == MeasureOfChange.FULL;
122
+ const mappedItemTracking = mapItem(
123
+ item,
124
+ index,
125
+ length,
126
+ force,
127
+ mappedItemsCache.get(item),
128
+ mapCallback
129
+ );
130
+ newMappedItemsCache.set(item, mappedItemTracking);
131
+ return mappedItemTracking.mappedItem;
132
+ });
133
+ mappedItemsCache = newMappedItemsCache;
134
+ return newMappedArray;
135
+ });
136
+ });
137
+ return mappedArray;
138
+ }
139
+ function createEvent(eventEffect) {
140
+ let handler;
141
+ let emitter = (h) => handler = h;
142
+ emitter.emit = (event) => handler && handler({ event });
143
+ if (eventEffect)
144
+ createEffect(() => eventEffect(emitter));
145
+ return emitter;
146
+ }
147
+ function provideContext(marker, context) {
148
+ currentHookContext().provideContexts.push([marker, context]);
149
+ }
150
+ function provideReactiveContext(marker, mkContext) {
151
+ const context = createReactiveContext(mkContext);
152
+ currentHookContext().provideContexts.push([marker, context]);
153
+ return context;
154
+ }
155
+ function materializeViewState(vsValueOrGetter) {
156
+ let vs = {};
157
+ for (let key in vsValueOrGetter) {
158
+ let value = vsValueOrGetter[key];
159
+ if (typeof value === "function")
160
+ value = value();
161
+ vs[key] = value;
162
+ }
163
+ return vs;
164
+ }
165
+ function renderWithContexts(provideContexts, render, viewState, index = 0) {
166
+ if (provideContexts.length > index) {
167
+ let [marker, context] = provideContexts[0];
168
+ return withContext(
169
+ marker,
170
+ context,
171
+ () => renderWithContexts(provideContexts, render, viewState, index + 1)
172
+ );
173
+ }
174
+ return render(viewState);
175
+ }
176
+ function mkMounts(componentContext, element) {
177
+ const [mounted, setMounted] = componentContext.mountedSignal;
178
+ componentContext.reactive.createReaction(() => {
179
+ if (mounted)
180
+ element.mount();
181
+ else
182
+ element.unmount();
183
+ });
184
+ const mount = () => {
185
+ componentContext.reactive.enable();
186
+ componentContext.reactive.batchReactions(() => setMounted(true));
187
+ };
188
+ const unmount = () => {
189
+ componentContext.reactive.batchReactions(() => setMounted(false));
190
+ componentContext.reactive.disable();
191
+ };
192
+ return [mount, unmount];
193
+ }
194
+ function makeJayComponent(preRender, comp, ...contextMarkers) {
195
+ return (props) => {
196
+ let componentInstance = null;
197
+ const getComponentInstance = () => {
198
+ return componentInstance;
199
+ };
200
+ const reactive = mkReactive();
201
+ const componentContext = {
202
+ mountedSignal: reactive.createSignal(true),
203
+ reactive,
204
+ provideContexts: [],
205
+ getComponentInstance
206
+ };
207
+ return withContext(COMPONENT_CONTEXT, componentContext, () => {
208
+ let propsProxy = makePropsProxy(componentContext.reactive, props);
209
+ let eventWrapper = (orig, event) => {
210
+ return componentContext.reactive.batchReactions(() => orig(event));
211
+ };
212
+ let [refs, render] = preRender({ eventWrapper });
213
+ let contexts = contextMarkers.map((marker) => {
214
+ const context = useContext(marker);
215
+ reactive.enablePairing(context[CONTEXT_REACTIVE_SYMBOL_CONTEXT]);
216
+ return context;
217
+ });
218
+ let coreComp = comp(propsProxy, refs, ...contexts);
219
+ let { render: renderViewState, ...api } = coreComp;
220
+ let element;
221
+ componentContext.provideContexts.forEach(
222
+ ([marker, context]) => context[CONTEXT_REACTIVE_SYMBOL_CONTEXT] && reactive.enablePairing(context[CONTEXT_REACTIVE_SYMBOL_CONTEXT])
223
+ );
224
+ componentContext.reactive.createReaction(() => {
225
+ let viewStateValueOrGetters = renderViewState();
226
+ let viewState = materializeViewState(viewStateValueOrGetters);
227
+ if (!element)
228
+ element = renderWithContexts(
229
+ componentContext.provideContexts,
230
+ render,
231
+ viewState
232
+ );
233
+ else
234
+ element.update(viewState);
235
+ });
236
+ const [mount, unmount] = mkMounts(componentContext, element);
237
+ let update = (updateProps) => {
238
+ propsProxy.update(updateProps);
239
+ };
240
+ let events = {};
241
+ let component = {
242
+ element,
243
+ update,
244
+ mount,
245
+ unmount,
246
+ addEventListener: (eventType, handler) => events[eventType](handler),
247
+ removeEventListener: (eventType) => events[eventType](void 0)
248
+ };
249
+ for (let key in api) {
250
+ if (typeof api[key] === "function") {
251
+ if (api[key].emit) {
252
+ component[key] = api[key];
253
+ if (key.indexOf("on") === 0) {
254
+ let [, , ...rest] = key;
255
+ events[rest.join("")] = api[key];
256
+ }
257
+ } else
258
+ component[key] = (...args) => componentContext.reactive.batchReactions(() => api[key](...args));
259
+ } else {
260
+ component[key] = api[key];
261
+ }
262
+ }
263
+ return componentInstance = component;
264
+ });
265
+ };
266
+ }
267
+ function makeJayTsxComponent(comp) {
268
+ return {};
269
+ }
270
+ function makePropsProxy(reactive, props) {
271
+ const stateMap = {};
272
+ const [_props, _setProps] = createSignal(props);
273
+ const update = (newProps) => {
274
+ reactive.batchReactions(() => {
275
+ _setProps(newProps);
276
+ for (const prop in newProps) {
277
+ if (!stateMap.hasOwnProperty(prop))
278
+ stateMap[prop] = reactive.createSignal(newProps[prop]);
279
+ else
280
+ stateMap[prop][1](newProps[prop]);
281
+ }
282
+ });
283
+ };
284
+ const getter = (obj, prop) => {
285
+ if (!stateMap.hasOwnProperty(prop))
286
+ stateMap[prop] = reactive.createSignal(obj[prop]);
287
+ return stateMap[prop][0];
288
+ };
289
+ return new Proxy(props, {
290
+ get: function(obj, prop) {
291
+ if (prop === "update")
292
+ return update;
293
+ else if (prop === "props")
294
+ return _props;
295
+ else
296
+ return getter(obj, prop);
297
+ }
298
+ });
299
+ }
300
+ const forTesting = {
301
+ makePropsProxy
302
+ };
303
+ export {
304
+ COMPONENT_CONTEXT,
305
+ CONTEXT_CREATION_CONTEXT,
306
+ CONTEXT_REACTIVE_SYMBOL_CONTEXT,
307
+ createDerivedArray,
308
+ createEffect,
309
+ createEvent,
310
+ createMemo,
311
+ createPatchableSignal,
312
+ createReactiveContext,
313
+ createSignal,
314
+ forTesting,
315
+ makeJayComponent,
316
+ makeJayTsxComponent,
317
+ makePropsProxy,
318
+ materializeViewState,
319
+ provideContext,
320
+ provideReactiveContext,
321
+ useReactive
322
+ };
@@ -0,0 +1,137 @@
1
+ # Creating Jay Components
2
+
3
+ Jay Component is defined in the `@jay-framework/runtime` library as anything that implements the interface
4
+
5
+ ```typescript
6
+ interface JayComponent<Props, ViewState, jayElement extends BaseJayElement<ViewState>> {
7
+ element: jayElement;
8
+ update: updateFunc<Props>;
9
+ mount: MountFunc;
10
+ unmount: MountFunc;
11
+ addEventListener: (type: string, handler: JayEventHandler<any, ViewState, void>) => void;
12
+ removeEventListener: (type: string, handler: JayEventHandler<any, ViewState, void>) => void;
13
+ }
14
+ ```
15
+
16
+ the `@jay-framework/component` library defines a reactive and elegant way to create headless Jay-Components.
17
+
18
+ ## Simple Example
19
+
20
+ ```typescript
21
+ import { render, CounterElementRefs } from './counter.jay-html';
22
+ import { createSignal, makeJayComponent, Props } from '@jay-framework/component';
23
+
24
+ export interface CounterProps {
25
+ initialValue: number;
26
+ }
27
+
28
+ function CounterConstructor({ initialValue }: Props<CounterProps>, refs: CounterElementRefs) {
29
+ const [count, setCount] = createSignal(initialValue);
30
+
31
+ refs.subtracter.onclick(() => setCount(count() - 1));
32
+ refs.adderButton.onclick(() => setCount(count() + 1));
33
+
34
+ return {
35
+ render: () => ({ count }),
36
+ };
37
+ }
38
+
39
+ export const Counter = makeJayComponent(render, CounterConstructor);
40
+ ```
41
+
42
+ The full example can be found at [counter.ts](../../../../examples/jay/counter/src/counter.ts)
43
+
44
+ ## The Component Constructor
45
+
46
+ The `@jay-framework/component` library offers constructing jay components using a component constructor function of the form
47
+
48
+ ```typescript
49
+ interface JayComponentCore<PropsT, ViewState> {
50
+ render: (props: Props<PropsT>) => ViewStateGetters<ViewState>;
51
+ }
52
+
53
+ type ComponentConstructor<
54
+ PropsT extends object,
55
+ Refs extends object,
56
+ ViewState extends object,
57
+ Contexts extends Array<any>,
58
+ CompCore extends JayComponentCore<PropsT, ViewState>,
59
+ > = (props: Props<PropsT>, refs: Refs, ...contexts: Contexts) => CompCore;
60
+ ```
61
+
62
+ ### Parameters:
63
+
64
+ - `props: Props<PropsT>`: an object holding signals for each of the component properties.
65
+ The `Props<T>` generic type turns an object of values into an object of signal getters.
66
+ - `refs: Refs`: the refs type from the associated Jay Element of the components
67
+ - `...contexts: Contexts`: The context instances requested by the `makeJayComponent` function
68
+
69
+ ### Returns
70
+
71
+ The function has to return an object implementing `JayComponentCore` which has to have a render function.
72
+ In addition, the returned object may have exported API functions and exported events of the component.
73
+
74
+ #### returned object render function
75
+
76
+ The returned `JayComponentCore` has to have a render function that returns `ViewStateGetters<ViewState>`.
77
+ The `ViewState` is the view state of the jay-element the component is using, and the `ViewStateGetters` allows
78
+ to provide the view state as values or getter functions.
79
+
80
+ both of the below are valid usages of render:
81
+
82
+ ```typescript
83
+ // as a signal
84
+ const [count, setCount] = createSignal(initialValue);
85
+ return {
86
+ render: () => ({ count }),
87
+ };
88
+
89
+ // as a value
90
+ return {
91
+ render: () => ({ count: count() }),
92
+ };
93
+ ```
94
+
95
+ The render function itself is reactive, allowing to perform calculations as part of the render function.
96
+
97
+ ```typescript
98
+ // using a computation
99
+ const [count, setCount] = createSignal(initialValue);
100
+ return {
101
+ render: () => ({ count, description: `the count is ${count()}` }),
102
+ };
103
+ ```
104
+
105
+ ## The `makeJayComponent` function
106
+
107
+ The `makeJayComponent` function returns a function that when called with `props`, will create a component instance.
108
+
109
+ ```typescript
110
+ declare function makeJayComponent<
111
+ PropsT extends object,
112
+ ViewState extends object,
113
+ Refs extends object,
114
+ JayElementT extends JayElement<ViewState, Refs>,
115
+ Contexts extends Array<any>,
116
+ CompCore extends JayComponentCore<PropsT, ViewState>,
117
+ >(
118
+ preRender: PreRenderElement<ViewState, Refs, JayElementT>,
119
+ comp: ComponentConstructor<PropsT, Refs, ViewState, Contexts, CompCore>,
120
+ ...contextMarkers: ContextMarkers<Contexts>
121
+ ): (props: PropsT) => ConcreteJayComponent<PropsT, ViewState, Refs, CompCore, JayElementT>;
122
+ ```
123
+
124
+ ### Parameters:
125
+
126
+ - `preRender: PreRenderElement<ViewState, Refs, JayElementT>`: The render function from a `JayElement` constructor.
127
+ - `comp: ComponentConstructor<PropsT, Refs, ViewState, Contexts, CompCore>`: the component constructor function.
128
+ - `...contextMarkers: ContextMarkers<Contexts>`: zero or more context markers created using `createJayContext`.
129
+ read more about providing contexts in [provide-context.md](./provide-context.md) and [provide-reactive-context.md](./provide-reactive-context.md).
130
+
131
+ ### Returns
132
+
133
+ A function that when called with props creates a component instance.
134
+
135
+ #### returned function parameters:
136
+
137
+ - `props: PropsT`: an object holding the properties for the component.
@@ -0,0 +1,54 @@
1
+ # createDerivedArray
2
+
3
+ Creates a derived mapped array that updates whenever the source array or its elements change.
4
+
5
+ When using immutable state, the `Array.map` function always creates a new array with new items.
6
+ `createDerivedArray` only recreates new items if the original item has changed, or a dependency of the mapping function
7
+ has changed. `createDerivedArray` is the reactive equivalent of `Array.map`.
8
+
9
+ The mapping function is recalculated if
10
+
11
+ - the original item has changed
12
+ - the item index has changed, and the mapping function is using the index signal
13
+ - the original array length has changed, and the mapping function is using the length signal
14
+ - any other signal the mapping function depends on has changed
15
+
16
+ ```typescript
17
+ declare function createDerivedArray<T extends object, U>(
18
+ arrayGetter: Getter<T[]>,
19
+ mapCallback: (item: Getter<T>, index: Getter<number>, length: Getter<number>) => U,
20
+ ): Getter<U[]>;
21
+ ```
22
+
23
+ ## Parameters:
24
+
25
+ - `arrayGetter: Getter<T[]>`: A getter function for the source array.
26
+ - `mapCallback`: A function that maps each item in the source array to a new value.
27
+ - `item: Getter<T>`: the original array item
28
+ - `index: Getter<number>`: the index of the original array item
29
+ - `length: Getter<number>`: the length of the original array
30
+
31
+ ## Returns:
32
+
33
+ A getter function for the derived array.
34
+
35
+ ## Example
36
+
37
+ ```typescript
38
+ const taskData = createDerivedArray(pillarTasks, (item, index, length) => {
39
+ let { id, title, description } = item();
40
+ return {
41
+ id,
42
+ taskProps: {
43
+ title,
44
+ description,
45
+ hasNext: hasNext(),
46
+ hasPrev: hasPrev(),
47
+ isBottom: index() === length() - 1,
48
+ isTop: index() === 0,
49
+ },
50
+ };
51
+ });
52
+ ```
53
+
54
+ See the full example in [Scrum Board/lib/pillar.ts](../../../../examples/jay/scrum-board/lib/pillar.ts).
@@ -0,0 +1,27 @@
1
+ # createEffect
2
+
3
+ Creates a side effect that runs after the component renders and cleans up when the component unmounts.
4
+
5
+ The effect function is called initially on hook creation, as well as if any of the signals it depends on change.
6
+
7
+ If the `effect` function returns a cleanup function, the cleanup function is called on component unmount or if
8
+ any of the signal the effect function depends on change, before recalling the effect function.
9
+
10
+ ```typescript
11
+ type EffectCleanup = () => void;
12
+ declare function createEffect(effect: () => void | EffectCleanup);
13
+ ```
14
+
15
+ ## Parameters:
16
+
17
+ - `effect`: A function that returns a cleanup function or undefined.
18
+
19
+ ## example:
20
+
21
+ ```typescript
22
+ createEffect(() => {
23
+ console.log('board', 'pillars:', pillars());
24
+ });
25
+ ```
26
+
27
+ See the full example in [Scrum Board/lib/board.ts](../../../../examples/jay/scrum-board/lib/board.ts).
@@ -0,0 +1,78 @@
1
+ # createEvent
2
+
3
+ Creates a Jay component event emitter.
4
+
5
+ Jay, unlike React or Solid.js, does not accept callbacks as props. Instead, the component constructor function has to
6
+ return as part of the returned object event emitters. The `createEvent` function is used to create those event emitters.
7
+
8
+ The event emitter itself is a function to register `JayEventHandler`s and has a member to `emit` events.
9
+
10
+ `createEvent` can optionally accept an `eventEffect` function which is used to create an effect given the event emitter.
11
+ It makes it simple to create an event that is triggered reactively based on the `eventEffect` effect function.
12
+
13
+ ```typescript
14
+ interface EventEmitter<EventType, ViewState> {
15
+ (handler: JayEventHandler<EventType, ViewState, void>): void;
16
+ emit: (event?: EventType) => void;
17
+ }
18
+
19
+ declare function createEvent<EventType>(
20
+ eventEffect?: (emitter: EventEmitter<EventType, any>) => void,
21
+ ): EventEmitter<EventType, any>;
22
+ ```
23
+
24
+ ## Parameters:
25
+
26
+ - `eventEffect`: An optional effect to run when the event is emitted.
27
+
28
+ ## Returns:
29
+
30
+ An event emitter object.
31
+
32
+ ## Examples:
33
+
34
+ A simple event emitter:
35
+
36
+ ```typescript
37
+ function TaskConstructor(props: Props<TaskProps>, refs: TaskElementRefs) {
38
+ let onNext = createEvent();
39
+ let onPrev = createEvent();
40
+ let onUp = createEvent();
41
+ let onDown = createEvent();
42
+
43
+ refs.next.onclick(() => onNext.emit());
44
+ refs.up.onclick(() => onUp.emit());
45
+ refs.down.onclick(() => onDown.emit());
46
+ refs.prev.onclick(() => onPrev.emit());
47
+
48
+ return {
49
+ render: () => props,
50
+ onNext,
51
+ onDown,
52
+ onUp,
53
+ onPrev,
54
+ };
55
+ }
56
+ ```
57
+
58
+ See the full example in [Scrum Board/lib/task.ts](../../../../examples/jay/scrum-board/lib/task.ts).
59
+
60
+ An event emitter with `eventEffect`:
61
+
62
+ ```typescript
63
+ function CounterComponent({}: Props<CounterProps>, refs: CounterRefs) {
64
+ let [value, setValue] = createSignal(0);
65
+ refs.inc.onclick(() => setValue(value() + 1));
66
+ refs.dec.onclick(() => setValue(value() - 1));
67
+ let onChange = createEvent<CounterChangeEvent>((emitter) => emitter.emit({ value: value() }));
68
+ return {
69
+ render: () => ({ value }),
70
+ onChange,
71
+ };
72
+ }
73
+ ```
74
+
75
+ In the above example, the `createEffect` parameter of `createEvent` is a function that reads the signal `value`, and
76
+ those the effect will run anytime the `value` signal changes, emitting an event.
77
+
78
+ See the full example in [component.test.ts](../test/component.test.ts).
@@ -0,0 +1,32 @@
1
+ # createMemo
2
+
3
+ Creates a memoized value that updates only when its dependencies change.
4
+
5
+ ```typescript
6
+ declare function createMemo<T>(computation: (prev: T) => T, initialValue?: T): Getter<T>;
7
+ ```
8
+
9
+ ## Parameters:
10
+
11
+ - `computation`: A function that calculates the new value based on the previous value.
12
+ - `initialValue`: The initial value of the memo.
13
+
14
+ ## Returns:
15
+
16
+ A getter function for the memoized value.
17
+
18
+ ## Example:
19
+
20
+ ```typescript
21
+ const activeTodoCount = createMemo(() =>
22
+ todos().reduce(function (accum: number, todo: ShownTodo) {
23
+ return todo.isCompleted ? accum : accum + 1;
24
+ }, 0),
25
+ );
26
+ const noActiveItems = createMemo(() => activeTodoCount() === 0);
27
+ const activeTodoWord = createMemo(() => (activeTodoCount() > 1 ? 'todos' : 'todo'));
28
+ const hasItems = createMemo(() => todos().length > 0);
29
+ const showClearCompleted = createMemo(() => !!todos().find((_) => _.isCompleted));
30
+ ```
31
+
32
+ See the full example in [Todo/lib/todo.ts](../../../../examples/jay/todo/lib/todo.ts).
@@ -0,0 +1,55 @@
1
+ # createPatchableSignal
2
+
3
+ Creates a reactive signal that also supports update using `JSONPatch`.
4
+
5
+ As Jay is using immutable state objects, when we need to update a deep structure of objects, we have to replace
6
+ the updated object and all parent objects. `createPatchableSignal` is a utility that is using the same `patch` from
7
+ `@jay-framework/json-patch` to simplify this update.
8
+
9
+ ```typescript
10
+ declare function createPatchableSignal<T>(
11
+ value: ValueOrGetter<T>,
12
+ ): [get: Getter<T>, set: Setter<T>, patchFunc: Patcher<T>];
13
+ ```
14
+
15
+ ## Parameters:
16
+
17
+ - `value`: The initial value of the signal.
18
+
19
+ ## Returns:
20
+
21
+ A tuple containing a `getter`, a `setter`, and a `patch` function.
22
+
23
+ - `getter: Getter<T>` function to get the signal value.
24
+ - `setter: Setter<T>` function to set the signal value. The setter function can receive a value or a `Next` function that
25
+ given the old signal value will calculate the next signal value.
26
+ - `patch: Patcher<T>` function to patch the signal value using `...JSONPatch` (spread json patch).
27
+
28
+ ## Examples:
29
+
30
+ ```typescript
31
+ const [todos, setTodos, patchTodos] = createPatchableSignal(
32
+ initialTodos().map((_) => ({ ..._, isEditing: false, editText: '' })),
33
+ );
34
+
35
+ // in some event handler
36
+ patchTodos(
37
+ { op: REPLACE, path: [itemIndex, 'title'], value: val },
38
+ { op: REPLACE, path: [itemIndex, 'isEditing'], value: false },
39
+ );
40
+
41
+ // in another event handler
42
+ patchTodos({
43
+ op: ADD,
44
+ path: [todos().length],
45
+ value: {
46
+ id: uuid(),
47
+ title: val,
48
+ isEditing: false,
49
+ editText: '',
50
+ isCompleted: false,
51
+ },
52
+ });
53
+ ```
54
+
55
+ See the full example in [todo-one-flat-component/lib/todo.ts](../../../../examples/jay/todo-one-flat-component/lib/todo.ts).
@@ -0,0 +1,44 @@
1
+ # createSignal
2
+
3
+ Creates a reactive signal. The signal can be initialized using a value or a `Getter`.
4
+
5
+ If initialized with a Getter, the signal will track the Getter's signal value, useful for signals tracking prop values
6
+ while allowing to update the signal value directly.
7
+
8
+ ```typescript
9
+ declare function createSignal<T>(value: ValueOrGetter<T>): [get: Getter<T>, set: Setter<T>];
10
+ ```
11
+
12
+ ## Parameters:
13
+
14
+ - `value`: The initial value of the signal, or a function that returns a value track.
15
+
16
+ If the value is a function, createSignal automatically wraps it with Reactive's createReaction
17
+ creating a dependency between any signals read within the function and the created signal.
18
+
19
+ ## Returns:
20
+
21
+ A tuple containing a `getter` and a `setter` for the signal's value.
22
+
23
+ - `getter: Getter<T>` function to get the signal value.
24
+ - `setter: Setter<T>` function to set the signal value. The setter function can receive a value or a `Next` function that
25
+ given the old signal value will calculate the next signal value.
26
+
27
+ ## Examples:
28
+
29
+ Creating a signal with an initial value
30
+
31
+ ```typescript
32
+ const [count, setCount] = createSignal(initialValue);
33
+ ```
34
+
35
+ Creating a signal that follows two other signal values
36
+
37
+ ```typescript
38
+ const [a, setA] = createSignal(10);
39
+ const [b, setB] = createSignal(20);
40
+ const [c, setC] = createSignal(() => a() + b());
41
+ ```
42
+
43
+ In the above example, the signal `c` can be updated using the `setC` setter, or it is updated automatically
44
+ whenever the signals `a` or `b` are updated.
@@ -0,0 +1,39 @@
1
+ # provideContext
2
+
3
+ Provides a context value to child components.
4
+
5
+ A Context created using `provideContext` is injected into child components who declare dependency on it using the same
6
+ context `marker`.
7
+
8
+ This context is not reactive, and any update to the context values will not trigger updates in any components.
9
+
10
+ - To trigger changes in components as a context changes, use the `provideReactiveContext` hook instead.
11
+ - This hook is great for dependency injection into child components.
12
+
13
+ ```typescript
14
+ declare function provideContext<ContextType>(
15
+ marker: ContextMarker<ContextType>,
16
+ context: ContextType,
17
+ );
18
+ ```
19
+
20
+ ## Parameters:
21
+
22
+ - `marker`: A unique symbol identifying the context, created using `createJayContext`.
23
+ - `context`: The context value to provide.
24
+
25
+ ## Example
26
+
27
+ ```typescript
28
+ const COUNT_CONTEXT = createJayContext<CountContext>();
29
+
30
+ // in a component constructor
31
+ provideContext(COUNT_CONTEXT, context);
32
+ ```
33
+
34
+ ## design log
35
+
36
+ For additional information on the design decisions of Jay Context API, read
37
+ [16 - context api.md](../../../../design-log/16%20-%20context%20api.md),
38
+ [21 - alternative to context API.md](../../../../design-log/21%20-%20alternative%20to%20context%20API.md),
39
+ [30 - Jay Context API.md](../../../../design-log/30%20-%20Jay%20Context%20API.md)
@@ -0,0 +1,68 @@
1
+ # provideReactiveContext
2
+
3
+ Provides a reactive context value to child components.
4
+
5
+ The reactive context is similar to a Jay Component by having it's own Reactive instance, allowing it to use any
6
+ of Jay Hooks (`createSignal`, `createEffect`, etc.) as well as export API functions.
7
+
8
+ - Child Components who read reactive context signal getters are said to be paired, and changes to such context signals
9
+ will trigger updates of components reading those signal values.
10
+ - API functions of reactive context are automatically wrapped by `reactive.batchReactions`.
11
+ - Best practice is to update a context using exported API functions as it ensures optimal computation of dependent effects.
12
+
13
+ ```typescript
14
+ declare function provideReactiveContext<ContextType extends object>(
15
+ marker: ContextMarker<ContextType>,
16
+ mkContext: () => ContextType,
17
+ ): ContextType;
18
+ ```
19
+
20
+ ## Parameters:
21
+
22
+ - `marker`: A unique symbol identifying the context, created using `createJayContext`.
23
+ - `mkContext`: A function that creates the initial context value.
24
+ The function may use any of the Jay Hooks and should return an object which represents the context.
25
+
26
+ ## Returns:
27
+
28
+ The created reactive context value.
29
+
30
+ ## Examples:
31
+
32
+ ```typescript
33
+ export const SCRUM_CONTEXT = createJayContext<ScrumContext>();
34
+ export const provideScrumContext = () =>
35
+ provideReactiveContext(SCRUM_CONTEXT, () => {
36
+ let [pillars, setPillars] = createSignal(DEFAULT_PILLARS);
37
+
38
+ const moveTaskToNext = (pillarId: string, taskId: string) => {
39
+ setPillars(moveTask(pillars(), pillarId, taskId, +1, 0));
40
+ };
41
+ const moveTaskToPrev = (pillarId: string, taskId: string) => {
42
+ setPillars(moveTask(pillars(), pillarId, taskId, -1, 0));
43
+ };
44
+ const moveTaskUp = (pillarId: string, taskId: string) => {
45
+ setPillars(moveTask(pillars(), pillarId, taskId, 0, +1));
46
+ };
47
+ const moveTaskDown = (pillarId: string, taskId: string) => {
48
+ setPillars(moveTask(pillars(), pillarId, taskId, 0, -1));
49
+ };
50
+
51
+ return {
52
+ pillars,
53
+ moveTaskToNext,
54
+ moveTaskToPrev,
55
+ moveTaskDown,
56
+ moveTaskUp,
57
+ };
58
+ });
59
+ ```
60
+
61
+ See the full example in [scrum-context.ts](../../../../examples/jay-context/scrum-board-with-context/lib/scrum-context.ts)
62
+
63
+ ## design log
64
+
65
+ For additional information on the design decisions of Jay Context API, read
66
+ [16 - context api.md](../../../../design-log/16%20-%20context%20api.md),
67
+ [21 - alternative to context API.md](../../../../design-log/21%20-%20alternative%20to%20context%20API.md),
68
+ [30 - Jay Context API.md](../../../../design-log/30%20-%20Jay%20Context%20API.md)
@@ -0,0 +1,13 @@
1
+ # useReactive
2
+
3
+ Gets the current reactive context of the current component or reactive context.
4
+
5
+ ```typescript
6
+ declare function useReactive(): Reactive;
7
+ ```
8
+
9
+ ## Returns:
10
+
11
+ The current reactive context object.
12
+
13
+ Read more about Jay Reactive in [reactive](..%2F..%2Freactive)
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@jay-framework/component",
3
+ "version": "0.5.0",
4
+ "type": "module",
5
+ "license": "Apache-2.0",
6
+ "main": "dist/index.js",
7
+ "files": [
8
+ "dist",
9
+ "docs",
10
+ "readme.md"
11
+ ],
12
+ "scripts": {
13
+ "build": "npm run build:js && npm run build:types",
14
+ "build:watch": "npm run build:js -- --watch & npm run build:types -- --watch",
15
+ "build:js": "vite build",
16
+ "build:types": "tsup lib/index.ts --dts-only --format esm",
17
+ "build:check-types": "tsc",
18
+ "clean": "rimraf dist",
19
+ "confirm": "npm run clean && npm run build && npm run build:check-types && npm run test",
20
+ "test": "vitest run",
21
+ "test:watch": "vitest"
22
+ },
23
+ "dependencies": {
24
+ "@jay-framework/json-patch": "workspace:^",
25
+ "@jay-framework/reactive": "workspace:^",
26
+ "@jay-framework/runtime": "workspace:^"
27
+ },
28
+ "devDependencies": {
29
+ "@jay-framework/dev-environment": "workspace:^",
30
+ "@testing-library/jest-dom": "^6.2.0",
31
+ "@types/node": "^20.11.5",
32
+ "rimraf": "^5.0.5",
33
+ "tsup": "^8.0.1",
34
+ "typescript": "^5.3.3",
35
+ "vite": "^5.0.11",
36
+ "vitest": "^1.2.1"
37
+ }
38
+ }
package/readme.md ADDED
@@ -0,0 +1,33 @@
1
+ # Jay Component
2
+
3
+ The Jay Component library defines the methods of constructing a Jay Component.
4
+
5
+ # Creating Components
6
+
7
+ - [Creating Jay Components](./docs/component.md)
8
+
9
+ # Jay Component Hooks
10
+
11
+ hooks to create signals:
12
+
13
+ - [createSignal](./docs/create-signal.md)
14
+ - [createPatchableSignal](./docs/create-patchable-signal.md)
15
+
16
+ hooks to create computed values:
17
+
18
+ - [createMemo](./docs/create-memo.md)
19
+ - [createDerivedArray](./docs/create-derived-array.md)
20
+
21
+ hooks to create reactions & events:
22
+
23
+ - [createEffect](./docs/create-effect.md)
24
+ - [createEvent](./docs/create-event.md)
25
+
26
+ Hooks for providing context:
27
+
28
+ - [provideContext](./docs/provide-context.md)
29
+ - [provideReactiveContext](./docs/provide-reactive-context.md)
30
+
31
+ Hooks to get Component Reactive Instance:
32
+
33
+ - [use-reactive](./docs/use-reactive.md)