react-native-unistyles
Version:
Level up your React Native StyleSheet
129 lines (97 loc) • 4.77 kB
text/typescript
import type { UnistyleDependency, UnistylesMiniRuntime } from '../specs'
import type { UnistylesTheme, UnistylesValues } from '../types'
import type { StyleSheet, StyleSheetWithSuperPowers } from '../types/stylesheet'
import { CSSState } from './css'
import type { UnistylesServices } from './types'
import { error, extractUnistyleDependencies, generateHash } from './utils'
export class UnistylesRegistry {
private readonly stylesheets = new Map<StyleSheetWithSuperPowers<StyleSheet>, StyleSheet>()
private readonly stylesCache = new Set<string>()
private readonly stylesCounter = new Map<string, Set<HTMLElement>>()
private readonly disposeListenersMap = new Map<object, VoidFunction>()
private readonly dependenciesMap = new Map<StyleSheetWithSuperPowers<StyleSheet>, Set<UnistyleDependency>>()
readonly css: CSSState
constructor(private services: UnistylesServices) {
this.css = new CSSState(services)
}
getComputedStylesheet = (stylesheet: StyleSheetWithSuperPowers<StyleSheet>, scopedThemeName?: UnistylesTheme) => {
if (typeof stylesheet !== 'function') {
return stylesheet
}
if (scopedThemeName) {
const scopedTheme = this.services.runtime.getTheme(scopedThemeName)
if (!scopedTheme) {
throw error(`Unistyles: You're trying to use scoped theme '${scopedThemeName}' but it wasn't registered.`)
}
return stylesheet(scopedTheme, this.services.runtime.miniRuntime)
}
const computedStylesheet = this.stylesheets.get(stylesheet)
if (computedStylesheet) {
return computedStylesheet
}
const currentTheme = this.services.runtime.getTheme(this.services.runtime.themeName, this.services.state.CSSVars)
const createdStylesheet = stylesheet(currentTheme, this.services.runtime.miniRuntime)
const dependencies = Object.values(createdStylesheet).flatMap(value => extractUnistyleDependencies(value))
this.addDependenciesToStylesheet(stylesheet, dependencies)
this.stylesheets.set(stylesheet, createdStylesheet)
return createdStylesheet
}
addDependenciesToStylesheet = (stylesheet: (theme: UnistylesTheme, miniRuntime: UnistylesMiniRuntime) => StyleSheet, dependencies: Array<UnistyleDependency>) => {
this.disposeListenersMap.get(stylesheet)?.()
const dependenciesMap = this.dependenciesMap.get(stylesheet) ?? new Set(dependencies)
dependencies.forEach(dependency => dependenciesMap.add(dependency))
const dispose = this.services.listener.addStylesheetListeners(Array.from(dependenciesMap), () => {
const newComputedStylesheet = stylesheet(
this.services.runtime.getTheme(this.services.runtime.themeName, this.services.state.CSSVars),
this.services.runtime.miniRuntime
)
this.stylesheets.set(stylesheet, newComputedStylesheet)
})
this.dependenciesMap.set(stylesheet, dependenciesMap)
this.disposeListenersMap.set(stylesheet, dispose)
}
connect = (ref: HTMLElement, hash: string) => {
const stylesCounter = this.stylesCounter.get(hash) ?? new Set()
stylesCounter.add(ref)
this.stylesCounter.set(hash, stylesCounter)
}
remove = (ref: HTMLElement, hash: string) => {
const stylesCounter = this.stylesCounter.get(hash) ?? new Set()
stylesCounter.delete(ref)
if (stylesCounter.size === 0) {
// Move this to the end of the event loop so the element is removed from the DOM
return Promise.resolve().then(() => {
// Check if element is still in the DOM
if (document.querySelector(`.${hash}`)) {
return false
}
this.css.remove(hash)
this.stylesCache.delete(hash)
return true
})
}
return Promise.resolve(false)
}
add = (value: UnistylesValues, forChild?: boolean) => {
const generatedHash = generateHash(value)
const hash = forChild
? `${generatedHash} > *`
: generatedHash
if (!this.stylesCache.has(hash)) {
this.applyStyles(hash, value)
this.stylesCache.add(hash)
return { hash, existingHash: false }
}
return { hash, existingHash: true }
}
applyStyles = (hash: string, value: UnistylesValues) => {
this.css.add(hash, value)
}
reset = () => {
this.css.reset()
this.stylesCache.clear()
this.dependenciesMap.clear()
this.disposeListenersMap.clear()
this.stylesCounter.clear()
}
}