Created
June 7, 2020 20:56
-
-
Save bradennapier/5efa451cd4842c4fff7dfebd078501f9 to your computer and use it in GitHub Desktop.
CSS Variable Provider for React for dynamic styling with potential to cascade
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 flattenObject from './flatten-object'; | |
export default function buildCSSVariables( | |
vars: Record<string, any>, | |
): { [key: string]: string | number } { | |
const flattened = flattenObject(vars, true, '--'); | |
return flattened; | |
} |
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 React from 'react'; | |
export const CSSVariableContext = /* #__PURE__ */ React.createContext( | |
null as any, | |
); | |
if (process.env.NODE_ENV !== 'production') { | |
CSSVariableContext.displayName = 'CSSVariable'; | |
} |
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 React from 'react'; | |
import { CSSVariableContext } from './components/Context'; | |
import { useVariableContext } from './utils/useVariableContext'; | |
type Props<V> = { | |
vars: V; | |
children: React.ReactNode; | |
}; | |
export default function CSSVariableProvider<V extends { [key: string]: any }>({ | |
vars, | |
children, | |
}: Props<V>): JSX.Element { | |
const [ref, setRef] = React.useState<HTMLDivElement | null>(null); | |
const setStyleRef = React.useCallback((node) => { | |
setRef(node); | |
}, []); | |
const contextValue = useVariableContext(vars, ref); | |
return ( | |
<CSSVariableContext.Provider value={contextValue}> | |
<div ref={setStyleRef} style={contextValue.styles.initial as any}> | |
{children} | |
</div> | |
</CSSVariableContext.Provider> | |
); | |
} |
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
/** | |
* Takes an object and reduces it so that it is a flat object of its keys | |
* { one: { two: 'three' } } --> { oneTwo: 'three' } | |
*/ | |
export default function flattenObject( | |
value: Record<string, any>, | |
toTitleCase = true, | |
prefix = '', | |
i = 0, | |
accum: Record<string, any> = {}, | |
): Record<string, string | any> { | |
return Object.keys(value).reduce((p, c) => { | |
let key: string; | |
if (toTitleCase && i !== 0) { | |
key = c.replace( | |
/\w\S*/g, | |
(txt) => txt.charAt(0).toUpperCase() + txt.substr(1), | |
); | |
} else { | |
key = c; | |
} | |
if (value[c] !== undefined && value[c] !== null) { | |
if (typeof value[c] === 'object') { | |
flattenObject(value[c], toTitleCase, `${prefix}${key}`, i + 1, p); | |
} else { | |
key = `${prefix}${key}`; | |
if (Object.prototype.hasOwnProperty.call(p, key)) { | |
throw new Error(`flattenVars failed due to key conflict with ${key}`); | |
} | |
// eslint-disable-next-line no-param-reassign | |
p[key] = value[c]; | |
} | |
} | |
return p; | |
}, accum); | |
} |
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
export type FlattenedObject< | |
V extends { [key: string]: any }, | |
O extends { | |
[key: string]: any; | |
} | |
> = { | |
[K in keyof O]: K extends keyof V | |
? V[K] extends { [key: string]: any } | |
? never | |
: V[K] | |
: string | number; | |
}; | |
export type ExactPartial< | |
V extends { [key: string]: any }, | |
C extends { | |
[key: string]: any; | |
} | |
> = { | |
[K in keyof C]: K extends keyof V | |
? V[K] extends { [key: string]: any } | |
? C[K] extends { [key: string]: any } | |
? ExactPartial<V[K], C[K]> | |
: Partial<V[K]> | |
: V[K] | |
: never; | |
}; | |
export type CSSVariableContext<V extends { [key: string]: any }> = { | |
styles: { | |
default: V; | |
current: { | |
[key: string]: string; | |
}; | |
}; | |
setStyleVars: <F, O>( | |
changes: F extends true ? FlattenedObject<V, O> : ExactPartial<V, O>, | |
isFlattened?: F | undefined, | |
) => any; | |
setStyleVar: ( | |
varName: string, | |
value: string | null, | |
priority?: string | undefined, | |
) => boolean; | |
getDefaultStyleVar: (name: string) => any; | |
getStyleVar: (name: string) => any; | |
}; |
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 { useContext } from 'react'; | |
import { CSSVariableContext } from '../components/Context'; | |
export default function useStyleVars(ctx = CSSVariableContext) { | |
const context = useContext(ctx); | |
return context; | |
} |
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 React from 'react'; | |
import { ExactPartial, FlattenedObject } from './types'; | |
import buildVariables from './buildVariables'; | |
const STYLES = Object.freeze({ | |
display: 'contents', | |
} as const); | |
function setStyleVars< | |
C extends { | |
[key: string]: any; | |
}, | |
F extends true | false | undefined | |
>( | |
stylesRef: React.RefObject<{ [key: string]: string | number }>, | |
ref: HTMLDivElement | null, | |
changes: C, | |
/** | |
* When set to true it indicates that the provided changes are already flattened | |
* allowing the flattenVariables step to be skipped. | |
*/ | |
isFlattened: F, | |
): any { | |
if (!ref || !stylesRef.current) { | |
return; | |
} | |
const flattened = isFlattened ? changes : buildVariables(changes); | |
for (const k in flattened) { | |
if (Object.prototype.hasOwnProperty.call(flattened, k)) { | |
const name = `--${k}`; | |
// console.log('Setting Variable: --', name, ' on ref: ', ref); | |
// eslint-disable-next-line no-param-reassign | |
stylesRef.current[name] = flattened[k]; | |
ref.style.setProperty(name, flattened[k]); | |
} | |
} | |
} | |
function createSetVariables<V extends { [key: string]: any }>( | |
ref: HTMLDivElement | null, | |
stylesRef: React.RefObject<{ [key: string]: string | number }>, | |
) { | |
return <F extends true | false, O extends { [key: string]: any }>( | |
changes: F extends true ? FlattenedObject<V, O> : ExactPartial<V, O>, | |
/** | |
* When set to true it indicates that the provided changes are already flattened | |
* allowing the flattenVariables step to be skipped. | |
*/ | |
isFlattened?: F, | |
) => | |
setStyleVars<typeof changes, F | undefined>( | |
stylesRef, | |
ref, | |
changes, | |
isFlattened, | |
); | |
} | |
export function useVariableContext<V extends { [key: string]: any }>( | |
vars: V, | |
ref: HTMLDivElement | null, | |
) { | |
const flattenedVars = React.useMemo(() => buildVariables(vars), [vars]); | |
const styles = React.useMemo(() => ({ ...STYLES, ...flattenedVars }), [ | |
flattenedVars, | |
]); | |
const stylesRef = React.useRef({ ...styles }); | |
const contextValue = React.useMemo(() => { | |
return { | |
styles: { | |
default: vars, | |
initial: styles, | |
}, | |
setStyleVars: createSetVariables<V>(ref, stylesRef), | |
setStyleVar: ( | |
varName: string, | |
value: string | null, | |
priority?: string, | |
) => { | |
if (!ref) { | |
return false; | |
} | |
const name = varName.startsWith('--') ? varName : `--${varName}`; | |
(stylesRef as any).current[name] = value; | |
ref.style.setProperty(name, value, priority); | |
return true; | |
}, | |
getDefaultStyleVar: (name: string) => | |
(styles as any)[name] || (styles as any)[`--${name}`], | |
getStyleVar: (name: string) => | |
stylesRef.current | |
? (stylesRef.current as any)[name] || | |
(stylesRef.current as any)[`--${name}`] | |
: null, | |
}; | |
}, [ref, stylesRef.current]); | |
return contextValue; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment