Skip to content

Commit 621521f

Browse files
committed
Started with context implementation
1 parent 00cba42 commit 621521f

File tree

4 files changed

+215
-4
lines changed

4 files changed

+215
-4
lines changed

src/main/js-element-hooks.ts

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { intercept, Ctrl } from 'js-element'
1+
import { intercept, Context, Ctrl } from 'js-element'
22

33
// === data ==========================================================
44

@@ -33,6 +33,16 @@ type SelectorsOf<S extends State, U extends Selectors<S>> = {
3333
[K in keyof U]: U[K] extends (state: S) => infer R ? R : never
3434
}
3535

36+
type CtxConfig = Record<string, Context<any> | (() => any)>
37+
38+
type ResultOfCtxConfig<C extends CtxConfig> = {
39+
[K in keyof C]: C[K] extends Context<infer R>
40+
? R
41+
: C[K] extends () => infer R
42+
? R
43+
: never
44+
}
45+
3646
// === hook ====================================================
3747

3848
export function hook<A extends any[], R, F extends { (...args: A): R }>(
@@ -510,3 +520,73 @@ function isEqualArray(arr1: any[], arr2: any[]) {
510520

511521
return ret
512522
}
523+
524+
// === context =======================================================
525+
526+
type ContextDetail<T> = {
527+
context: Context<T>
528+
callback: (newValue: T) => void
529+
cancelled: Promise<null>
530+
}
531+
532+
export const useCtx = hook('useCtx', useCtxFn)
533+
534+
function useCtxFn<C extends CtxConfig>(config: C): ResultOfCtxConfig<C>
535+
536+
function useCtxFn<T>(ctx: Context<T>): () => T
537+
538+
function useCtxFn(arg: any): any {
539+
if (arg && arg.kind === 'context') {
540+
return withConsumer(arg)
541+
}
542+
543+
const ret: any = {}
544+
545+
Object.entries(arg).forEach(([k, v]) => {
546+
Object.defineProperty(ret, k, {
547+
get:
548+
(v as any).kind === 'context'
549+
? withConsumer(v as any)
550+
: (arg[k] as () => any)
551+
})
552+
})
553+
554+
return ret
555+
}
556+
557+
function withConsumer<T>(ctx: Context<T>): () => T {
558+
const c = currentCtrl!
559+
const host = c.getHost()
560+
let cancel: null | (() => void) = null
561+
562+
const cancelled = new Promise<null>((resolve) => {
563+
cancel = () => resolve(null)
564+
})
565+
566+
let value = ctx.defaultValue
567+
568+
c.beforeMount(() => {
569+
const detail: ContextDetail<T> = {
570+
context: ctx,
571+
572+
callback: (newValue: T) => {
573+
value = newValue
574+
c.refresh()
575+
},
576+
577+
cancelled
578+
}
579+
580+
host.dispatchEvent(
581+
new CustomEvent('$$context$$', {
582+
detail,
583+
bubbles: true,
584+
composed: true
585+
})
586+
)
587+
})
588+
589+
c.beforeUnmount(() => cancel!())
590+
591+
return () => value! // TODO
592+
}

src/main/js-element.ts

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
// === exports =======================================================
22

33
// functions and singletons
4-
export { component, elem, intercept, prop, setMethods, Attrs }
4+
export {
5+
component,
6+
createCtx,
7+
defineProvider,
8+
elem,
9+
intercept,
10+
prop,
11+
setMethods,
12+
Attrs
13+
}
514

615
// types
7-
export { Ctrl, MethodsOf }
16+
export { Context, Ctrl, MethodsOf }
817

918
// === data ==========================================================
1019

@@ -67,6 +76,11 @@ type Ctrl = {
6776

6877
type InterceptFn = (ctrl: Ctrl, next: () => void) => void
6978

79+
type Context<T> = {
80+
kind: 'context'
81+
defaultValue: T
82+
}
83+
7084
// === decorators (all public) =======================================
7185

7286
function elem<E extends Component, C>(params: {
@@ -486,3 +500,74 @@ function registerElement(
486500
customElements.define(tagName, elementClass)
487501
}
488502
}
503+
504+
// === context =======================================================
505+
506+
function createCtx<T>(defaultValue?: T): Context<T> {
507+
return Object.freeze({
508+
kind: 'context',
509+
defaultValue: defaultValue!
510+
})
511+
}
512+
513+
function defineProvider<T>(
514+
tagName: string,
515+
ctx: Context<T>
516+
): { new (): HTMLElement & { value: T | undefined } } {
517+
const eventName = `$$context$$`
518+
519+
class CtxProviderElement extends HTMLElement {
520+
private __value?: T = undefined
521+
private __subscribers: ((value: T) => void)[] = []
522+
private __cleanup: (() => void) | null = null
523+
524+
constructor() {
525+
super()
526+
this.attachShadow({ mode: 'open' })
527+
}
528+
529+
get value(): T | undefined {
530+
return this.__value
531+
}
532+
533+
set value(val: T | undefined) {
534+
if (val !== this.__value) {
535+
this.__value = val
536+
this.__subscribers.forEach((subscriber) => subscriber(val!))
537+
}
538+
}
539+
540+
connectedCallback() {
541+
this.shadowRoot!.innerHTML = '<slot></slot>'
542+
543+
const eventListener = (ev: any) => {
544+
if (ev.detail.context !== ctx) {
545+
return
546+
}
547+
548+
ev.stopPropagation()
549+
this.__subscribers.push(ev.detail.callback)
550+
551+
ev.detail.cancelled.then(() => {
552+
this.__subscribers.splice(
553+
this.__subscribers.indexOf(ev.detail.callback),
554+
1
555+
)
556+
})
557+
}
558+
559+
this.addEventListener(eventName, eventListener)
560+
this.__cleanup = () => this.removeEventListener(eventName, eventListener)
561+
}
562+
563+
disconnectCallback() {
564+
this.__subscribers.length === 0
565+
this.__cleanup!()
566+
this.__cleanup = null
567+
}
568+
}
569+
570+
registerElement(tagName, CtxProviderElement)
571+
572+
return CtxProviderElement
573+
}

src/stories/demos/context-demo.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { component, createCtx, defineProvider, elem, prop } from 'js-element'
2+
import { useCtx, useInterval, useState } from 'js-element/hooks'
3+
import { html, withLit } from 'js-element/lit'
4+
5+
const ThemeCtx = createCtx('light')
6+
const ThemeProvider = defineProvider('theme-provider', ThemeCtx)
7+
8+
@elem({
9+
tag: 'contxt-demo',
10+
impl: withLit(implContextDemo)
11+
})
12+
class ContextDemo extends component() {}
13+
14+
function implContextDemo(self: ContextDemo) {
15+
const [state, setState] = useState({ theme: 'light' })
16+
17+
useInterval(() => {
18+
setState('theme', (it) => (it === 'light' ? 'dark' : 'light'))
19+
}, 1000)
20+
21+
return () => html`
22+
<div>
23+
<b>Value for theme will change every second:</b>
24+
<br />
25+
<theme-provider .value=${state.theme}>
26+
<theme-info />
27+
</theme-provider>
28+
</div>
29+
`
30+
}
31+
32+
@elem({
33+
tag: 'theme-info',
34+
impl: withLit(implThemeInfo)
35+
})
36+
class ThemeInfo extends component() {}
37+
38+
function implThemeInfo() {
39+
const ctx = useCtx({ theme: ThemeCtx })
40+
41+
return () => html`<div>Current theme: ${ctx.theme}</div>`
42+
}
43+
44+
export default ContextDemo

src/stories/index.stories.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import CounterDemo from './demos/counter-demo'
2+
import ContextDemo from './demos/context-demo'
23

34
export default {
45
title: 'Demos'
@@ -10,4 +11,5 @@ function demo(demoClass: any) {
1011
`<div><${tagName}></${tagName}></div><br><div id="message"></div>`
1112
}
1213

13-
export const Test = demo(CounterDemo)
14+
export const counter = demo(CounterDemo)
15+
export const context = demo(ContextDemo)

0 commit comments

Comments
 (0)