Skip to content

Commit fc5e89b

Browse files
committed
feat: allow DOM elements as insertion point
1 parent 8403680 commit fc5e89b

6 files changed

Lines changed: 126 additions & 10 deletions

File tree

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,50 @@ const App = () => {
133133
};
134134
```
135135

136+
### HTML Element Insertion Point
137+
138+
Some libraries and frameworks make it hard to use comments in head for handling injection order. To solve this issue, you can provide a DOM element as the insertion point. Take for example a `<noscript></noscript>` element:
139+
140+
```html
141+
<!DOCTYPE html>
142+
<html lang="en">
143+
<head>
144+
<style>
145+
@import url('https://fonts.googleapis.com/css2?family=Poppins&display=swap');
146+
147+
* {
148+
color: inherit;
149+
}
150+
151+
html {
152+
font-family: 'Poppins', sans-serif;
153+
}
154+
</style>
155+
<noscript id="inject-styles-here"></noscript>
156+
157+
<title>Playground</title>
158+
</head>
159+
160+
<body>
161+
<div id="root"></div>
162+
</body>
163+
</html>
164+
```
165+
166+
```jsx
167+
const App = () => {
168+
return (
169+
<ThemeSwitcherProvider
170+
defaultTheme="light"
171+
insertionPoint={document.getElementById('inject-styles-here')}
172+
themeMap={themes}
173+
>
174+
<Component />
175+
</ThemeSwitcherProvider>
176+
);
177+
};
178+
```
179+
136180
## API
137181

138182
### ThemeSwitcherProvider

example/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
font-family: 'Poppins', sans-serif;
1616
}
1717
</style>
18-
<!-- inject-styles-here -->
18+
<noscript id="inject-styles-here"></noscript>
1919

2020
<title>Playground</title>
2121
</head>

example/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ const themes = {
8282
const App = () => {
8383
return (
8484
<ThemeSwitcherProvider
85-
insertionPoint="inject-styles-here"
85+
insertionPoint={document.getElementById('inject-styles-here')}
8686
themeMap={themes}
8787
defaultTheme={'dark'}
8888
>

src/index.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import * as React from 'react';
2-
import { findCommentNode, arrayToObject, createLinkElement } from './utils';
2+
import {
3+
findCommentNode,
4+
arrayToObject,
5+
createLinkElement,
6+
isElement,
7+
} from './utils';
38

49
enum Status {
510
idle = 'idle',
@@ -21,7 +26,7 @@ const ThemeSwitcherContext = React.createContext<
2126
interface Props {
2227
themeMap: Record<any, string>;
2328
children?: React.ReactNode;
24-
insertionPoint?: string;
29+
insertionPoint?: string | HTMLElement | null;
2530
id?: string;
2631
defaultTheme?: string;
2732
attr?: string;
@@ -40,8 +45,10 @@ export function ThemeSwitcherProvider({
4045

4146
const insertStyle = React.useCallback(
4247
(linkElement: HTMLElement): HTMLElement | void => {
43-
if (insertionPoint) {
44-
const insertionPointElement = findCommentNode(insertionPoint);
48+
if (insertionPoint || insertionPoint === null) {
49+
const insertionPointElement = isElement(insertionPoint)
50+
? (insertionPoint as HTMLElement)
51+
: findCommentNode(insertionPoint as string);
4552

4653
if (!insertionPointElement) {
4754
console.warn(
@@ -106,7 +113,7 @@ export function ThemeSwitcherProvider({
106113
React.useEffect(() => {
107114
const themes = Object.keys(themeMap);
108115

109-
themes.map(theme => {
116+
themes.map((theme) => {
110117
const themeAssetId = `theme-prefetch-${theme}`;
111118
if (!document.getElementById(themeAssetId)) {
112119
const stylePrefetch = document.createElement('link');

src/utils.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,19 @@ export function findCommentNode(comment: string) {
99
return null;
1010
}
1111

12+
export function isElement(o: any) {
13+
return typeof HTMLElement === 'object'
14+
? o instanceof HTMLElement //DOM2
15+
: o &&
16+
typeof o === 'object' &&
17+
o !== null &&
18+
o.nodeType === 1 &&
19+
typeof o.nodeName === 'string';
20+
}
21+
1222
export function arrayToObject(array: string[]): Record<any, string> {
1323
const obj: Record<any, string> = {};
14-
array.forEach(el => (obj[el] = el));
24+
array.forEach((el) => (obj[el] = el));
1525
return obj;
1626
}
1727

test/index.test.tsx

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ describe('Theme switcher', () => {
170170
);
171171
});
172172

173-
it('should insert styles on insertionPoint', () => {
173+
it('should insert styles on comment insertionPoint', () => {
174174
const insertionPoint = 'insert-style-here';
175175

176176
document.head.insertBefore(
@@ -211,7 +211,46 @@ describe('Theme switcher', () => {
211211
});
212212
});
213213

214-
it('should warn when insertionPoint does not exist', () => {
214+
it('should insert styles on DOM insertionPoint', () => {
215+
const insertionPoint = document.createElement('noscript');
216+
insertionPoint.id = 'style-insertion-point';
217+
218+
document.head.insertBefore(insertionPoint, document.head.firstElementChild);
219+
220+
const otherElement = document.createElement('meta');
221+
document.head.append(otherElement);
222+
renderHook(() => useThemeSwitcher(), {
223+
wrapper: Wrapper({
224+
themeMap: themes,
225+
defaultTheme: 'dark',
226+
insertionPoint: document.getElementById(insertionPoint.id) ?? undefined,
227+
}),
228+
});
229+
230+
function allPreviousElements(element: Element | null) {
231+
const previousElements: HTMLLinkElement[] = [];
232+
while ((element = element!.previousElementSibling))
233+
previousElements.push(element as HTMLLinkElement);
234+
235+
return previousElements;
236+
}
237+
238+
document.getElementById(insertionPoint.id)?.remove();
239+
const element = document.head.lastElementChild;
240+
const previousElements = allPreviousElements(element);
241+
242+
previousElements.forEach(elem => {
243+
const conditions = [
244+
'current-theme-style',
245+
'theme-prefetch-dark',
246+
'theme-prefetch-light',
247+
];
248+
const found = conditions.some(condition => elem.id === condition);
249+
expect(found).toBeTruthy();
250+
});
251+
});
252+
253+
it('should warn when comment insertionPoint does not exist', () => {
215254
renderHook(() => useThemeSwitcher(), {
216255
wrapper: Wrapper({
217256
themeMap: themes,
@@ -227,6 +266,22 @@ describe('Theme switcher', () => {
227266
);
228267
});
229268

269+
it('should warn when DOM insertionPoint does not exist', () => {
270+
renderHook(() => useThemeSwitcher(), {
271+
wrapper: Wrapper({
272+
themeMap: themes,
273+
defaultTheme: 'dark',
274+
insertionPoint: document.getElementById('not-in-dom-insertion-point'),
275+
}),
276+
});
277+
278+
expect(console.warn).toHaveBeenCalledTimes(3);
279+
// @ts-ignore
280+
expect(console.warn.mock.calls[0][0]).toMatchInlineSnapshot(
281+
`"Insertion point 'null' does not exist. Be sure to add comment on head and that it matches the insertionPoint"`
282+
);
283+
});
284+
230285
it('should append to head even when insertionPoint does not exist', async () => {
231286
const { wait } = renderHook(() => useThemeSwitcher(), {
232287
wrapper: Wrapper({

0 commit comments

Comments
 (0)