Last active
June 30, 2020 19:08
-
-
Save lorefnon/53377e4d6a6b13adbcfa155f486946a3 to your computer and use it in GitHub Desktop.
React ElFac
This file contains 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 { 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") | |
); |
This file contains 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 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