Last active
April 22, 2021 11:35
-
-
Save srph/afcebf6f233ed3ef8280c9208ab4310b to your computer and use it in GitHub Desktop.
Shogun Frontend: Signals
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { memo, MouseEvent, RefObject, useEffect, useMemo } from 'react' | |
import { useRef } from 'react' | |
//#region utils | |
/** | |
* We need a notion of reactivity. React is not reactive. | |
*/ | |
interface Signal<A> { | |
(handler: (a: A) => void): () => void | |
} | |
function newSignal<A>(): [signal: Signal<A>, next: (value: A) => void] { | |
const handlers = new Set<(value: A) => void>() | |
return [ | |
(handler) => { | |
handlers.add(handler) | |
return () => handlers.delete(handler) | |
}, | |
(value) => handlers.forEach((handler) => handler(value)), | |
] | |
} | |
function useSignal<A>(signal: Signal<A>, handler: (a: A) => void): void { | |
const dispose = useMemo(() => signal(handler), [signal, handler]) | |
useEffect(() => dispose, [dispose]) | |
} | |
const useScroll = (ref: RefObject<HTMLElement>, handler: () => void): void => { | |
useEffect(() => { | |
const target = ref.current | |
target?.addEventListener('scroll', handler, { | |
passive: true, | |
}) | |
return () => { | |
target?.removeEventListener('scroll', handler) | |
} | |
}, []) | |
} | |
//#endregion | |
//#region view model | |
interface ViewModel { | |
readonly scrollTo: Signal<number> | |
readonly handleContainerScroll: (scrollTop: number) => void | |
readonly handleHeadingClose: (headingScrollTop: number) => void | |
} | |
const newAccordionViewModel = (): ViewModel => { | |
let containerScrollTop = 0 | |
const [scrollTo, next] = newSignal<number>() | |
return { | |
scrollTo, | |
handleContainerScroll: (scrollTop) => (containerScrollTop = scrollTop), | |
handleHeadingClose: (headingScrollTop) => { | |
if (containerScrollTop > 0) { | |
next(headingScrollTop) | |
} | |
}, | |
} | |
} | |
//#endregion | |
//#region view | |
const TestView = memo(() => { | |
const vm = useMemo(() => newAccordionViewModel(), []) | |
/** | |
* Scrollable container | |
*/ | |
const containerRef = useRef<HTMLDivElement>(null) | |
//#region bindings | |
const { handleScroll, handleToggle, handleScrollTop } = useMemo( | |
() => ({ | |
handleScroll: () => containerRef.current && vm.handleContainerScroll(containerRef.current.scrollTop), | |
handleToggle: (e: MouseEvent<HTMLDivElement>) => vm.handleHeadingClose(e.currentTarget.scrollTop), | |
handleScrollTop: (scrollTop: number) => | |
containerRef.current?.scrollTo({ | |
top: scrollTop, | |
}), | |
}), | |
[], | |
) | |
useScroll(containerRef, handleScroll) | |
useSignal(vm.scrollTo, handleScrollTop) | |
//#endregion | |
return ( | |
<div ref={containerRef}> | |
<div onClick={handleToggle}>Heading</div> | |
</div> | |
) | |
}) | |
//#endregion |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment