Skip to content

Instantly share code, notes, and snippets.

@srph
Last active April 22, 2021 11:35
Show Gist options
  • Save srph/afcebf6f233ed3ef8280c9208ab4310b to your computer and use it in GitHub Desktop.
Save srph/afcebf6f233ed3ef8280c9208ab4310b to your computer and use it in GitHub Desktop.
Shogun Frontend: Signals
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