Skip to content

Instantly share code, notes, and snippets.

@lorefnon
Last active June 30, 2020 19:08
Show Gist options
  • Save lorefnon/53377e4d6a6b13adbcfa155f486946a3 to your computer and use it in GitHub Desktop.
Save lorefnon/53377e4d6a6b13adbcfa155f486946a3 to your computer and use it in GitHub Desktop.
React ElFac
import { render } from "react-dom";
import { factoryMap } from "./react-elfac";
import App from "./App";
// Use factoryMap to wrap one or more imported components
const R = factoryMap({
App
});
// By default factories of all the normal html tags will also be
// available through R
render(
// Use template literals to add classes and ids
// through css style selectors
R.div`.container.container-bordered`({
// For rest of the props, we'd want to benefit from
// type-safety, so they can be passed through an object
id: "top-container",
// Children can be passed as just another prop
children: [
// When you need only children, wrap provides a more
// succinct API to wrap a bunch of children
R.div.wrap(
// Of course, this works with simple text nodes
R.span.wrap("Hello"),
// And we can combine tagged templates and wrap
R.b`.strong`.wrap("World")
),
// The components we added to factoryMap will
// be available through a similar unified API
R.App({})
]
}),
document.getElementById("root")
);
import memoize from "lodash/memoize"
import isArray from "lodash/isArray"
import assign from "lodash/assign"
import {
AnchorHTMLAttributes,
HTMLAttributes,
AreaHTMLAttributes,
AudioHTMLAttributes,
BaseHTMLAttributes,
BlockquoteHTMLAttributes,
ButtonHTMLAttributes,
CanvasHTMLAttributes,
ColHTMLAttributes,
ColgroupHTMLAttributes,
DataHTMLAttributes,
DelHTMLAttributes,
DetailsHTMLAttributes,
DialogHTMLAttributes,
EmbedHTMLAttributes,
FieldsetHTMLAttributes,
FormHTMLAttributes,
HtmlHTMLAttributes,
IframeHTMLAttributes,
ImgHTMLAttributes,
InputHTMLAttributes,
InsHTMLAttributes,
KeygenHTMLAttributes,
LabelHTMLAttributes,
LiHTMLAttributes,
LinkHTMLAttributes,
MapHTMLAttributes,
MenuHTMLAttributes,
MetaHTMLAttributes,
MeterHTMLAttributes,
ObjectHTMLAttributes,
OlHTMLAttributes,
OptgroupHTMLAttributes,
OptionHTMLAttributes,
OutputHTMLAttributes,
ParamHTMLAttributes,
ProgressHTMLAttributes,
QuoteHTMLAttributes,
SlotHTMLAttributes,
ScriptHTMLAttributes,
SelectHTMLAttributes,
SourceHTMLAttributes,
StyleHTMLAttributes,
TableHTMLAttributes,
TdHTMLAttributes,
TextareaHTMLAttributes,
ThHTMLAttributes,
TimeHTMLAttributes,
TrackHTMLAttributes,
VideoHTMLAttributes,
WebViewHTMLAttributes,
SVGAttributes,
ReactElement,
createElement,
ReactChild,
} from "react"
interface HTMLAttributesMapping {
a: AnchorHTMLAttributes<HTMLAnchorElement>
abbr: HTMLAttributes<HTMLElement>
address: HTMLAttributes<HTMLElement>
area: AreaHTMLAttributes<HTMLAreaElement>
article: HTMLAttributes<HTMLElement>
aside: HTMLAttributes<HTMLElement>
audio: AudioHTMLAttributes<HTMLAudioElement>
b: HTMLAttributes<HTMLElement>
base: BaseHTMLAttributes<HTMLBaseElement>
bdi: HTMLAttributes<HTMLElement>
bdo: HTMLAttributes<HTMLElement>
big: HTMLAttributes<HTMLElement>
blockquote: BlockquoteHTMLAttributes<HTMLElement>
body: HTMLAttributes<HTMLBodyElement>
br: HTMLAttributes<HTMLBRElement>
button: ButtonHTMLAttributes<HTMLButtonElement>
canvas: CanvasHTMLAttributes<HTMLCanvasElement>
caption: HTMLAttributes<HTMLElement>
cite: HTMLAttributes<HTMLElement>
code: HTMLAttributes<HTMLElement>
col: ColHTMLAttributes<HTMLTableColElement>
colgroup: ColgroupHTMLAttributes<HTMLTableColElement>
data: DataHTMLAttributes<HTMLDataElement>
datalist: HTMLAttributes<HTMLDataListElement>
dd: HTMLAttributes<HTMLElement>
del: DelHTMLAttributes<HTMLElement>
details: DetailsHTMLAttributes<HTMLElement>
dfn: HTMLAttributes<HTMLElement>
dialog: DialogHTMLAttributes<HTMLDialogElement>
div: HTMLAttributes<HTMLDivElement>
dl: HTMLAttributes<HTMLDListElement>
dt: HTMLAttributes<HTMLElement>
em: HTMLAttributes<HTMLElement>
embed: EmbedHTMLAttributes<HTMLEmbedElement>
fieldset: FieldsetHTMLAttributes<HTMLFieldSetElement>
figcaption: HTMLAttributes<HTMLElement>
figure: HTMLAttributes<HTMLElement>
footer: HTMLAttributes<HTMLElement>
form: FormHTMLAttributes<HTMLFormElement>
h1: HTMLAttributes<HTMLHeadingElement>
h2: HTMLAttributes<HTMLHeadingElement>
h3: HTMLAttributes<HTMLHeadingElement>
h4: HTMLAttributes<HTMLHeadingElement>
h5: HTMLAttributes<HTMLHeadingElement>
h6: HTMLAttributes<HTMLHeadingElement>
head: HTMLAttributes<HTMLElement>
header: HTMLAttributes<HTMLElement>
hgroup: HTMLAttributes<HTMLElement>
hr: HTMLAttributes<HTMLHRElement>
html: HtmlHTMLAttributes<HTMLHtmlElement>
i: HTMLAttributes<HTMLElement>
iframe: IframeHTMLAttributes<HTMLIFrameElement>
img: ImgHTMLAttributes<HTMLImageElement>
input: InputHTMLAttributes<HTMLInputElement>
ins: InsHTMLAttributes<HTMLModElement>
kbd: HTMLAttributes<HTMLElement>
keygen: KeygenHTMLAttributes<HTMLElement>
label: LabelHTMLAttributes<HTMLLabelElement>
legend: HTMLAttributes<HTMLLegendElement>
li: LiHTMLAttributes<HTMLLIElement>
link: LinkHTMLAttributes<HTMLLinkElement>
main: HTMLAttributes<HTMLElement>
map: MapHTMLAttributes<HTMLMapElement>
mark: HTMLAttributes<HTMLElement>
menu: MenuHTMLAttributes<HTMLElement>
menuitem: HTMLAttributes<HTMLElement>
meta: MetaHTMLAttributes<HTMLMetaElement>
meter: MeterHTMLAttributes<HTMLElement>
nav: HTMLAttributes<HTMLElement>
noscript: HTMLAttributes<HTMLElement>
object: ObjectHTMLAttributes<HTMLObjectElement>
ol: OlHTMLAttributes<HTMLOListElement>
optgroup: OptgroupHTMLAttributes<HTMLOptGroupElement>
option: OptionHTMLAttributes<HTMLOptionElement>
output: OutputHTMLAttributes<HTMLElement>
p: HTMLAttributes<HTMLParagraphElement>
param: ParamHTMLAttributes<HTMLParamElement>
picture: HTMLAttributes<HTMLElement>
pre: HTMLAttributes<HTMLPreElement>
progress: ProgressHTMLAttributes<HTMLProgressElement>
q: QuoteHTMLAttributes<HTMLQuoteElement>
rp: HTMLAttributes<HTMLElement>
rt: HTMLAttributes<HTMLElement>
ruby: HTMLAttributes<HTMLElement>
s: HTMLAttributes<HTMLElement>
samp: HTMLAttributes<HTMLElement>
slot: SlotHTMLAttributes<HTMLSlotElement>
script: ScriptHTMLAttributes<HTMLScriptElement>
section: HTMLAttributes<HTMLElement>
select: SelectHTMLAttributes<HTMLSelectElement>
small: HTMLAttributes<HTMLElement>
source: SourceHTMLAttributes<HTMLSourceElement>
span: HTMLAttributes<HTMLSpanElement>
strong: HTMLAttributes<HTMLElement>
style: StyleHTMLAttributes<HTMLStyleElement>
sub: HTMLAttributes<HTMLElement>
summary: HTMLAttributes<HTMLElement>
sup: HTMLAttributes<HTMLElement>
table: TableHTMLAttributes<HTMLTableElement>
template: HTMLAttributes<HTMLTemplateElement>
tbody: HTMLAttributes<HTMLTableSectionElement>
td: TdHTMLAttributes<HTMLTableDataCellElement>
textarea: TextareaHTMLAttributes<HTMLTextAreaElement>
tfoot: HTMLAttributes<HTMLTableSectionElement>
th: ThHTMLAttributes<HTMLTableHeaderCellElement>
thead: HTMLAttributes<HTMLTableSectionElement>
time: TimeHTMLAttributes<HTMLElement>
title: HTMLAttributes<HTMLTitleElement>
tr: HTMLAttributes<HTMLTableRowElement>
track: TrackHTMLAttributes<HTMLTrackElement>
u: HTMLAttributes<HTMLElement>
ul: HTMLAttributes<HTMLUListElement>
var: HTMLAttributes<HTMLElement>
video: VideoHTMLAttributes<HTMLVideoElement>
wbr: HTMLAttributes<HTMLElement>
webview: WebViewHTMLAttributes<HTMLWebViewElement>
}
type HTMLTagName = keyof HTMLAttributesMapping
interface SVGAttributesMapping {
animate: SVGAttributes<SVGAnimateElement>
circle: SVGAttributes<SVGCircleElement>
clipPath: SVGAttributes<SVGClipPathElement>
defs: SVGAttributes<SVGDefsElement>
desc: SVGAttributes<SVGDescElement>
ellipse: SVGAttributes<SVGEllipseElement>
feBlend: SVGAttributes<SVGFEBlendElement>
feColorMatrix: SVGAttributes<SVGFEColorMatrixElement>
feComponentTransfer: SVGAttributes<SVGFEComponentTransferElement>
feComposite: SVGAttributes<SVGFECompositeElement>
feConvolveMatrix: SVGAttributes<SVGFEConvolveMatrixElement>
feDiffuseLighting: SVGAttributes<SVGFEDiffuseLightingElement>
feDisplacementMap: SVGAttributes<SVGFEDisplacementMapElement>
feDistantLight: SVGAttributes<SVGFEDistantLightElement>
feDropShadow: SVGAttributes<SVGFEDropShadowElement>
feFlood: SVGAttributes<SVGFEFloodElement>
feFuncA: SVGAttributes<SVGFEFuncAElement>
feFuncB: SVGAttributes<SVGFEFuncBElement>
feFuncG: SVGAttributes<SVGFEFuncGElement>
feFuncR: SVGAttributes<SVGFEFuncRElement>
feGaussianBlur: SVGAttributes<SVGFEGaussianBlurElement>
feImage: SVGAttributes<SVGFEImageElement>
feMerge: SVGAttributes<SVGFEMergeElement>
feMergeNode: SVGAttributes<SVGFEMergeNodeElement>
feMorphology: SVGAttributes<SVGFEMorphologyElement>
feOffset: SVGAttributes<SVGFEOffsetElement>
fePointLight: SVGAttributes<SVGFEPointLightElement>
feSpecularLighting: SVGAttributes<SVGFESpecularLightingElement>
feSpotLight: SVGAttributes<SVGFESpotLightElement>
feTile: SVGAttributes<SVGFETileElement>
feTurbulence: SVGAttributes<SVGFETurbulenceElement>
filter: SVGAttributes<SVGFilterElement>
foreignObject: SVGAttributes<SVGForeignObjectElement>
g: SVGAttributes<SVGGElement>
image: SVGAttributes<SVGImageElement>
line: SVGAttributes<SVGLineElement>
linearGradient: SVGAttributes<SVGLinearGradientElement>
marker: SVGAttributes<SVGMarkerElement>
mask: SVGAttributes<SVGMaskElement>
metadata: SVGAttributes<SVGMetadataElement>
path: SVGAttributes<SVGPathElement>
pattern: SVGAttributes<SVGPatternElement>
polygon: SVGAttributes<SVGPolygonElement>
polyline: SVGAttributes<SVGPolylineElement>
radialGradient: SVGAttributes<SVGRadialGradientElement>
rect: SVGAttributes<SVGRectElement>
stop: SVGAttributes<SVGStopElement>
svg: SVGAttributes<SVGSVGElement>
switch: SVGAttributes<SVGSwitchElement>
symbol: SVGAttributes<SVGSymbolElement>
text: SVGAttributes<SVGTextElement>
textPath: SVGAttributes<SVGTextPathElement>
tspan: SVGAttributes<SVGTSpanElement>
use: SVGAttributes<SVGUseElement>
view: SVGAttributes<SVGViewElement>
}
type SVGTagName = keyof SVGAttributesMapping
interface Props2El<P> {
(props: P): ReactElement
wrap(...children: ReactChild[]): ReactElement
}
interface SelectorAugmentedProps2El<P> extends Props2El<P> {
(template: TemplateStringsArray, ...args: any[]): Props2El<P>
}
type MaybeSelectorAugmented<P, TSel> = TSel extends true
? SelectorAugmentedProps2El<P>
: Props2El<P>
export function factory<P, TSel extends boolean = false>(
component: React.FunctionComponent<P>,
isSelectorAugmented?: TSel
): MaybeSelectorAugmented<P, TSel>
export function factory<P, S, TSel extends boolean = false>(
component: React.ComponentClass<P, S>,
isSelectorAugmented?: TSel
): MaybeSelectorAugmented<P, TSel>
export function factory<
C extends keyof HTMLAttributesMapping,
TSel extends boolean = false
>(
component: C,
isSelectorAugmented?: TSel
): MaybeSelectorAugmented<HTMLAttributesMapping[C], TSel>
export function factory<
C extends keyof SVGAttributesMapping,
TSel extends boolean = false
>(
component: C,
isSelectorAugmented?: TSel
): MaybeSelectorAugmented<SVGAttributesMapping[C], TSel>
export function factory<TSel extends boolean = false>(
component: string,
isSelectorAugmented?: TSel
): MaybeSelectorAugmented<HTMLAttributes<HTMLElement>, TSel>
export function factory(
component: unknown,
isSelectorAugmented?: boolean
): unknown {
const c = component as any
const fn: any = isSelectorAugmented
? (a0: any, ...a1: any[]) => {
if (isArray(a0)) {
const selector = combineTmplParts(
(a0 as any) as TemplateStringsArray,
a1
)
const { id, classes } = parseSelector(selector)
const fn: any = (props: any) => {
if (props.className) classes.push(props.className)
return createElement(c, {
id,
...props,
className: classes.join(" "),
})
}
fn.wrap = (...children: ReactChild[]) =>
createElement(c, {
id,
className: classes.join(" ")
}, ...children)
return fn;
}
return createElement(c, a0)
}
: (props: any) => createElement(c, props)
fn.wrap = (...children: ReactChild[]) =>
createElement(c, undefined, ...children)
return fn
}
const htmlTags: (keyof HTMLAttributesMapping)[] = [
"a",
"abbr",
"address",
"area",
"article",
"aside",
"audio",
"b",
"base",
"bdi",
"bdo",
"big",
"blockquote",
"body",
"br",
"button",
"canvas",
"caption",
"cite",
"code",
"col",
"colgroup",
"data",
"datalist",
"dd",
"del",
"details",
"dfn",
"dialog",
"div",
"dl",
"dt",
"em",
"embed",
"fieldset",
"figcaption",
"figure",
"footer",
"form",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"head",
"header",
"hgroup",
"hr",
"html",
"i",
"iframe",
"img",
"input",
"ins",
"kbd",
"keygen",
"label",
"legend",
"li",
"link",
"main",
"map",
"mark",
"menu",
"menuitem",
"meta",
"meter",
"nav",
"noscript",
"object",
"ol",
"optgroup",
"option",
"output",
"p",
"param",
"picture",
"pre",
"progress",
"q",
"rp",
"rt",
"ruby",
"s",
"samp",
"slot",
"script",
"section",
"select",
"small",
"source",
"span",
"strong",
"style",
"sub",
"summary",
"sup",
"table",
"template",
"tbody",
"td",
"textarea",
"tfoot",
"th",
"thead",
"time",
"title",
"tr",
"track",
"u",
"ul",
"var",
"video",
"wbr",
"webview",
]
const svgTags: (keyof SVGAttributesMapping)[] = [
"animate",
"circle",
"clipPath",
"defs",
"desc",
"ellipse",
"feBlend",
"feColorMatrix",
"feComponentTransfer",
"feComposite",
"feConvolveMatrix",
"feDiffuseLighting",
"feDisplacementMap",
"feDistantLight",
"feDropShadow",
"feFlood",
"feFuncA",
"feFuncB",
"feFuncG",
"feFuncR",
"feGaussianBlur",
"feImage",
"feMerge",
"feMergeNode",
"feMorphology",
"feOffset",
"fePointLight",
"feSpecularLighting",
"feSpotLight",
"feTile",
"feTurbulence",
"filter",
"foreignObject",
"g",
"image",
"line",
"linearGradient",
"marker",
"mask",
"metadata",
"path",
"pattern",
"polygon",
"polyline",
"radialGradient",
"rect",
"stop",
"svg",
"switch",
"symbol",
"text",
"textPath",
"tspan",
"use",
"view",
]
const combineTmplParts = (
selectorTmpl: TemplateStringsArray,
selectorTmplParts: any[]
) => {
let selector = ""
let selectorTmplIdx = 0
let selectorTmplPartsIdx = 0
while (true) {
let f1 = false
let f2 = false
if (selectorTmplIdx < selectorTmpl.length) {
selector += selectorTmpl[selectorTmplIdx++]
} else f1 = true
if (selectorTmplPartsIdx < selectorTmplParts.length) {
selector += selectorTmplParts[selectorTmplPartsIdx++]
} else f2 = true
if (f1 && f2) break
}
return selector
}
const parseSelector = (selector: string) => {
const classes: string[] = []
let id: string | null = null
const selectorParts = selector.split(/(\.|#)([a-z0-9_-]+)/i)
let idx = 0
while (true) {
if (selectorParts[idx] !== "" || !selectorParts[idx + 2])
throw new Error(
"Malformed selector: only class & id expressions are supported"
)
switch (selectorParts[idx + 1]) {
case ".":
classes.push(selectorParts[idx + 2])
break
case "#":
if (id) {
throw new Error("Only one id can be assigned")
}
id = selectorParts[idx + 2]
break
default:
throw new Error(
"Malformed selector: only class & id expressions are supported"
)
}
idx += 3
if (idx >= selectorParts.length - 1) break
}
return { id, classes }
}
const buildProps2ElMapping = <T extends string>(tags: T[]) => {
const result: any = {}
for (const c of tags) {
result[c] = factory(c, true)
result[c].wrap = (...children: ReactChild[]) =>
createElement(c, undefined, ...children)
}
return result
}
const htmlProps2ElMapping = memoize((): {
[K in HTMLTagName]: SelectorAugmentedProps2El<HTMLAttributesMapping[K]>
} => buildProps2ElMapping(htmlTags))
const svgProps2ElMapping = memoize((): {
[K in SVGTagName]: SelectorAugmentedProps2El<SVGAttributesMapping[K]>
} => buildProps2ElMapping(svgTags))
const defaultProps2ElMapping = (): SelectorAugmentedProps2El<"div"> &
ReturnType<typeof htmlProps2ElMapping> &
ReturnType<typeof svgProps2ElMapping> => {
const fn: any = factory("div", true)
assign(fn, htmlProps2ElMapping())
assign(fn, svgProps2ElMapping())
return fn
}
const memoizedDefaultProps2ElMapping = memoize(defaultProps2ElMapping)
export function factoryMap<
C extends { [key: string]: any },
TIncludeDefault extends boolean = true,
TSel extends boolean = false
>(
components?: C,
options?: {
includeDefault?: TIncludeDefault
selectorAugmented?: TSel
}
): {
[K in keyof C]: C[K] extends React.FunctionComponent<infer P>
? Props2El<P>
: C[K] extends React.ComponentClass<infer P, infer _S>
? Props2El<P>
: C[K] extends keyof HTMLAttributesMapping
? Props2El<HTMLAttributesMapping[C[K]]>
: C[K] extends keyof SVGAttributesMapping
? Props2El<SVGAttributesMapping[C[K]]>
: C[K] extends string
? Props2El<HTMLAttributes<HTMLElement>>
: never
} &
(TIncludeDefault extends true
? ReturnType<typeof defaultProps2ElMapping>
: {}) {
const includeDefault = options?.includeDefault !== false
const selectorAugmented = options?.selectorAugmented === true
if (!components && includeDefault !== false)
return memoizedDefaultProps2ElMapping as any
const result: any = includeDefault ? defaultProps2ElMapping() : {}
if (components) {
for (const [key, Component] of Object.entries(components)) {
result[key] = factory(Component, selectorAugmented)
}
}
return result
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment