Skip to content

Commit ee69f74

Browse files
authored
Rearrange package and metadata (#6)
* Rename `observable-map` file * Rename `context-protocol` file * Rename mixins file to be the index file * Set package name and description * Typecheck all files * Update references in tests
1 parent a6d5bef commit ee69f74

11 files changed

+207
-209
lines changed

context-protocol.ts

+67-12
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,73 @@
1-
type Subscriber<T> = (value: T) => void;
1+
// From: https://github.com/webcomponents-cg/community-protocols/blob/main/proposals/context.md#definitions
22

3-
export class ObservableMap {
4-
#store = new Map<string, {value: unknown, subscribers: Set<Subscriber<unknown>>}>
3+
/**
4+
* A Context object defines an optional initial value for a Context, as well as a name identifier for debugging purposes.
5+
*/
6+
export type Context<T> = {
7+
name: string;
8+
initialValue?: T;
9+
};
510

6-
set(key: string, value: unknown, subscribers = new Set<Subscriber<unknown>>()) {
7-
const data = this.#store.get(key);
8-
subscribers = new Set([...subscribers, ...(data?.subscribers || new Set())]);
9-
this.#store.set(key, {value, subscribers});
10-
for (const subscriber of subscribers) {
11-
subscriber(value);
12-
}
11+
/**
12+
* An unknown context type
13+
*/
14+
export type UnknownContext = Context<unknown>;
15+
16+
/**
17+
* A helper type which can extract a Context value type from a Context type
18+
*/
19+
export type ContextType<T extends UnknownContext> = T extends Context<infer Y>
20+
? Y
21+
: never;
22+
23+
/**
24+
* A function which creates a Context value object
25+
*/
26+
export function createContext<T>(
27+
name: string,
28+
initialValue?: T
29+
): Readonly<Context<T>> {
30+
return {
31+
name,
32+
initialValue,
33+
};
34+
}
35+
36+
/**
37+
* A callback which is provided by a context requester and is called with the value satisfying the request.
38+
* This callback can be called multiple times by context providers as the requested value is changed.
39+
*/
40+
export type ContextCallback<ValueType> = (
41+
value: ValueType,
42+
unsubscribe?: () => void
43+
) => void;
44+
45+
/**
46+
* An event fired by a context requester to signal it desires a named context.
47+
*
48+
* A provider should inspect the `context` property of the event to determine if it has a value that can
49+
* satisfy the request, calling the `callback` with the requested value if so.
50+
*
51+
* If the requested context event contains a truthy `subscribe` value, then a provider can call the callback
52+
* multiple times if the value is changed, if this is the case the provider should pass an `unsubscribe`
53+
* function to the callback which requesters can invoke to indicate they no longer wish to receive these updates.
54+
*/
55+
export class ContextEvent<T extends UnknownContext> extends Event {
56+
public constructor(
57+
public readonly context: T,
58+
public readonly callback: ContextCallback<ContextType<T>>,
59+
public readonly subscribe?: boolean
60+
) {
61+
super("context-request", { bubbles: true, composed: true });
1362
}
63+
}
1464

15-
get(key: string) {
16-
return this.#store.get(key);
65+
declare global {
66+
interface HTMLElementEventMap {
67+
/**
68+
* A 'context-request' event can be emitted by any element which desires
69+
* a context value to be injected by an external provider.
70+
*/
71+
"context-request": ContextEvent<UnknownContext>;
1772
}
1873
}

index.ts

+111-68
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,116 @@
1-
// From: https://github.com/webcomponents-cg/community-protocols/blob/main/proposals/context.md#definitions
2-
3-
/**
4-
* A Context object defines an optional initial value for a Context, as well as a name identifier for debugging purposes.
5-
*/
6-
export type Context<T> = {
7-
name: string;
8-
initialValue?: T;
9-
};
10-
11-
/**
12-
* An unknown context type
13-
*/
14-
export type UnknownContext = Context<unknown>;
15-
16-
/**
17-
* A helper type which can extract a Context value type from a Context type
18-
*/
19-
export type ContextType<T extends UnknownContext> = T extends Context<infer Y>
20-
? Y
21-
: never;
22-
23-
/**
24-
* A function which creates a Context value object
25-
*/
26-
export function createContext<T>(
27-
name: string,
28-
initialValue?: T
29-
): Readonly<Context<T>> {
30-
return {
31-
name,
32-
initialValue,
33-
};
1+
import { ObservableMap } from "./observable-map.js";
2+
import { createContext, ContextEvent, UnknownContext } from "./context-protocol.js";
3+
4+
interface CustomElement extends HTMLElement {
5+
new (...args: any[]): CustomElement;
6+
observedAttributes: string[];
7+
connectedCallback?(): void;
8+
attributeChangedCallback?(
9+
name: string,
10+
oldValue: string | null,
11+
newValue: string | null,
12+
): void;
13+
disconnectedCallback?(): void;
14+
adoptedCallback?(): void;
3415
}
3516

36-
/**
37-
* A callback which is provided by a context requester and is called with the value satisfying the request.
38-
* This callback can be called multiple times by context providers as the requested value is changed.
39-
*/
40-
export type ContextCallback<ValueType> = (
41-
value: ValueType,
42-
unsubscribe?: () => void
43-
) => void;
44-
45-
/**
46-
* An event fired by a context requester to signal it desires a named context.
47-
*
48-
* A provider should inspect the `context` property of the event to determine if it has a value that can
49-
* satisfy the request, calling the `callback` with the requested value if so.
50-
*
51-
* If the requested context event contains a truthy `subscribe` value, then a provider can call the callback
52-
* multiple times if the value is changed, if this is the case the provider should pass an `unsubscribe`
53-
* function to the callback which requesters can invoke to indicate they no longer wish to receive these updates.
54-
*/
55-
export class ContextEvent<T extends UnknownContext> extends Event {
56-
public constructor(
57-
public readonly context: T,
58-
public readonly callback: ContextCallback<ContextType<T>>,
59-
public readonly subscribe?: boolean
60-
) {
61-
super("context-request", { bubbles: true, composed: true });
62-
}
17+
export function ProviderMixin(Class: CustomElement) {
18+
return class extends Class {
19+
#dataStore = new ObservableMap();
20+
21+
connectedCallback() {
22+
super.connectedCallback?.();
23+
24+
// @ts-expect-error todo
25+
for (const [key, value] of Object.entries(this.contexts || {})) {
26+
// @ts-expect-error todo
27+
this.#dataStore.set(key, value());
28+
}
29+
30+
this.addEventListener('context-request', this);
31+
}
32+
33+
disconnectedCallback(): void {
34+
this.#dataStore = new ObservableMap();
35+
}
36+
37+
handleEvent(event: Event) {
38+
if (event.type === "context-request") {
39+
this.#handleContextRequest(event as ContextEvent<UnknownContext>);
40+
}
41+
}
42+
43+
updateContext(name: string, value: unknown) {
44+
this.#dataStore.set(name, value);
45+
}
46+
47+
// We listen for a bubbled context request event and provide the event with the context requested.
48+
#handleContextRequest(
49+
event: ContextEvent<{ name: string; initialValue?: unknown }>,
50+
) {
51+
const { name, initialValue } = event.context;
52+
const subscribe = event.subscribe;
53+
if (initialValue) {
54+
this.#dataStore.set(name, initialValue);
55+
}
56+
const data = this.#dataStore.get(name);
57+
if (data) {
58+
event.stopPropagation();
59+
60+
let unsubscribe = () => undefined;
61+
62+
if (subscribe) {
63+
unsubscribe = () => {
64+
data.subscribers.delete(event.callback);
65+
};
66+
data.subscribers.add(event.callback);
67+
}
68+
69+
event.callback(data.value, unsubscribe);
70+
}
71+
}
72+
};
6373
}
6474

65-
declare global {
66-
interface HTMLElementEventMap {
67-
/**
68-
* A 'context-request' event can be emitted by any element which desires
69-
* a context value to be injected by an external provider.
70-
*/
71-
"context-request": ContextEvent<UnknownContext>;
72-
}
75+
export function ConsumerMixin(Class: CustomElement) {
76+
return class extends Class {
77+
unsubscribes: Array<() => void> = [];
78+
79+
connectedCallback() {
80+
super.connectedCallback?.();
81+
82+
// @ts-expect-error don't worry about it babe
83+
for (const [contextName, callback] of Object.entries(this.contexts)) {
84+
const context = createContext(contextName);
85+
86+
// We dispatch a event with that context. The event will bubble up the tree until it
87+
// reaches a component that is able to provide that value to us.
88+
// The event has a callback for the the value.
89+
this.dispatchEvent(
90+
new ContextEvent(
91+
context,
92+
(data, unsubscribe) => {
93+
// @ts-expect-error
94+
callback(data);
95+
if (unsubscribe) {
96+
this.unsubscribes.push(unsubscribe);
97+
}
98+
},
99+
// Always subscribe. Consumers can ignore updates if they'd like.
100+
true,
101+
),
102+
);
103+
}
104+
}
105+
106+
// Unsubscribe from all callbacks when disconnecting
107+
disconnectedCallback() {
108+
for (const unsubscribe of this.unsubscribes) {
109+
unsubscribe?.();
110+
}
111+
// Empty out the array in case this element is still stored in memory but just not connected
112+
// to the DOM.
113+
this.unsubscribes = [];
114+
}
115+
};
73116
}

mixins.ts

-116
This file was deleted.

0 commit comments

Comments
 (0)