Skip to content

Commit b2cc461

Browse files
committed
Support for elements with namespaceURI added
1 parent dda1527 commit b2cc461

File tree

10 files changed

+2100
-1639
lines changed

10 files changed

+2100
-1639
lines changed

README.md

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,37 @@ or `tsconfig.json`
3838

3939
then coding js or ts with jsx.
4040

41+
## context
42+
43+
Some JSX elements require context from their parents to be rendered correctly. Examples are children of an SVG element that require a namespaceURI from the parent.
44+
The setContext method is designed to resolve this issue.
45+
46+
```tsx
47+
const SvgContext = (props: OptionsChildren, ctx: any) => (props.children as ContextFunc)(ctx);
48+
49+
export default (props: OptionsChildren) => {
50+
setContext(SvgContext, {})
51+
return <SvgContext><svg xmlns='http://www.w3.org/2000/svg' width='1' height='1'>
52+
<path d={props.children}></path>
53+
</svg></SvgContext>;
54+
}
55+
```
56+
or even simpler
57+
58+
```jsx
59+
export default props => {
60+
setContext('svg', {})
61+
return <svg xmlns='http://www.w3.org/2000/svg' width='1' height='1'>
62+
<path d={props.children}></path>
63+
</svg>;
64+
}
65+
```
66+
4167
## examples
4268

4369
The source codes are in the repository https://github.com/only-jsx/examples.
4470

45-
```js
71+
```jsx
4672
//index.jsx
4773
import APP from './app';
4874

@@ -76,7 +102,7 @@ export default ({props, children}) => {
76102
<span>{props.a + props.b}</span>
77103
<button onclick={onClick}>Button</button>
78104
<div ref={result}></div>
79-
<div id='counter' style="color: blue;"></div>
105+
<div id='counter' style='color: blue;'></div>
80106
{children}
81107
</div>;
82108

index.test.ts

Lines changed: 103 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@ import {
99
TagFunc,
1010
JsxRef,
1111
Options,
12+
OptionsChildren,
13+
ContextFunc,
1214
} from './index';
1315

1416
describe('Test Runtime', () => {
17+
beforeEach(clearContext);
18+
1519
test('context', () => {
1620
const ctx = 1;
1721
setContext('div', ctx);
@@ -20,6 +24,11 @@ describe('Test Runtime', () => {
2024
expect(getContext()).toBeNull();
2125
});
2226

27+
test('wrong context', () => {
28+
const ctx = { namespaceURI: '' };
29+
expect(() => setContext('div', ctx)).toThrowError('namespaceURI context property is reserved for internal use');
30+
});
31+
2332
test('jsx children', () => {
2433
const testDivOptions = (options: Options, html: string) => {
2534
let r1 = jsx('div', options);
@@ -112,8 +121,6 @@ describe('Test Runtime', () => {
112121

113122
test('jsx with event', () => {
114123
const onclick = jest.fn();
115-
116-
let r: JsxRef = {};
117124
const r1 = jsx('div', { id: 1, children: true, onclick });
118125
expect(r1 instanceof HTMLDivElement).toBeTruthy();
119126
const e1 = r1 as HTMLElement;
@@ -126,7 +133,6 @@ describe('Test Runtime', () => {
126133
});
127134

128135
test('jsx with attribute', () => {
129-
let r: JsxRef = {};
130136
const r1 = jsx('div', { id: 1, children: true, style: 'display: block' });
131137
expect(r1 instanceof HTMLDivElement).toBeTruthy();
132138
const e1 = r1 as HTMLElement;
@@ -135,7 +141,6 @@ describe('Test Runtime', () => {
135141
});
136142

137143
test('jsx with null attribute', () => {
138-
let r: JsxRef = {};
139144
const r1 = jsx('div', { id: 1, children: true, style: null });
140145
expect(r1 instanceof HTMLDivElement).toBeTruthy();
141146
const e1 = r1 as HTMLElement;
@@ -144,42 +149,129 @@ describe('Test Runtime', () => {
144149
});
145150

146151
test('jsx with undefined attribute', () => {
147-
let r: JsxRef = {};
148152
const r1 = jsx('div', { id: 1, children: true, style: undefined });
149153
expect(r1 instanceof HTMLDivElement).toBeTruthy();
150154
const e1 = r1 as HTMLElement;
151155
expect(e1.outerHTML).toBe('<div id="1">true</div>');
152156
expect(e1.style.display).toBe('')
153157
});
154158

159+
test('jsx with namespace', () => {
160+
const fc: TagFunc = (o: Options, ctx?: any) => (o.children as Function)(ctx);
161+
162+
setContext(fc, {});
163+
const r1 = jsx(fc, { children: jsx('svg', { id: 1, children: jsx('path', { id: 1, d: 'MZ', fill: 'white' }), xmlns: 'http://www.w3.org/2000/svg' }) });
164+
165+
expect(r1 instanceof SVGElement).toBeTruthy();
166+
const e1 = r1 as SVGElement;
167+
expect(e1.outerHTML).toBe('<svg id="1" xmlns="http://www.w3.org/2000/svg"><path id="1" d="MZ" fill="white"></path></svg>');
168+
expect(e1.namespaceURI).toBe('http://www.w3.org/2000/svg');
169+
expect(e1.firstChild instanceof Element).toBeTruthy();
170+
const e2 = e1.firstChild as Element;
171+
expect(e2.namespaceURI).toBe('http://www.w3.org/2000/svg');
172+
});
173+
174+
test('jsx with nested namespaces', () => {
175+
const fc = (o: OptionsChildren, ctx?: any) => (o.children as ContextFunc)(ctx);
176+
177+
setContext(fc, {});
178+
179+
const nested = jsx('h1', { id: 31, children: jsx('h2', { id: 32, children: jsx('h3', {}), xmlns: null }), xmlns: '3' });
180+
const nested1 = jsx('h1', { id: 21, children: jsx('h2', { id: 22, children: jsx('h3', { children: nested }), xmlns: null }), xmlns: '' });
181+
const nested2 = jsx('h1', { id: 11, children: jsx('h2', { id: 12, children: jsx('h3', { children: nested1 }) }), xmlns: '2' });
182+
const r1 = jsx(fc, { children: jsx('h1', { id: 1, children: jsx('h2', { id: 2, children: jsx('h3', { children: nested2 }) }), xmlns: '1' }) });
183+
184+
expect(r1 instanceof Element).toBeTruthy();
185+
const e1 = r1 as Element;
186+
expect(e1.outerHTML).toBe('<h1 id="1" xmlns="1"><h2 id="2"><h3><h1 id="11" xmlns="2"><h2 id="12"><h3><h1 id="21" xmlns=""><h2 id="22"><h3><h1 id="31" xmlns="3"><h2 id="32"><h3></h3></h2></h1></h3></h2></h1></h3></h2></h1></h3></h2></h1>');
187+
expect(e1.namespaceURI).toBe('1');
188+
expect(e1.firstChild instanceof Element).toBeTruthy();
189+
const e2 = e1.firstChild as Element;
190+
expect(e2.namespaceURI).toBe('1');
191+
expect(e2.firstChild instanceof Element).toBeTruthy();
192+
const e3 = e2.firstChild as Element;
193+
expect(e3.namespaceURI).toBe('1');
194+
195+
const e11 = e3.firstChild as Element;
196+
expect(e11.namespaceURI).toBe('2');
197+
expect(e11.firstChild instanceof Element).toBeTruthy();
198+
const e12 = e11.firstChild as Element;
199+
expect(e12.namespaceURI).toBe('2');
200+
expect(e12.firstChild instanceof Element).toBeTruthy();
201+
const e13 = e12.firstChild as Element;
202+
expect(e13.namespaceURI).toBe('2');
203+
204+
const e21 = e13.firstChild as Element;
205+
expect(e21.namespaceURI).toBeNull();
206+
expect(e21.firstChild instanceof Element).toBeTruthy();
207+
const e22 = e21.firstChild as Element;
208+
expect(e22.namespaceURI).toBeNull();
209+
expect(e22.firstChild instanceof Element).toBeTruthy();
210+
const e23 = e22.firstChild as Element;
211+
expect(e23.namespaceURI).toBeNull();
212+
213+
const e31 = e23.firstChild as Element;
214+
expect(e31.namespaceURI).toBe('3');
215+
expect(e31.firstChild instanceof Element).toBeTruthy();
216+
const e32 = e31.firstChild as Element;
217+
expect(e32.namespaceURI).toBe('3');
218+
expect(e32.firstChild instanceof Element).toBeTruthy();
219+
const e33 = e32.firstChild as Element;
220+
expect(e33.namespaceURI).toBe('3');
221+
});
222+
223+
test('exception without context', () => {
224+
expect(() => jsx('svg', { xmlns: '1' })).toThrowError('Declaring a namespace on an element using xmlns: attribute requires context');
225+
});
226+
155227
test('jsx with context', () => {
156228
const r1 = jsx('div', { id: 1, children: null });
157229
expect(r1 instanceof HTMLDivElement).toBeTruthy();
158230
const e1 = r1 as HTMLElement;
159231
expect(e1.outerHTML).toBe('<div id="1"></div>');
160232

161-
const fc: TagFunc = () => e1;
233+
const fc: TagFunc = (options, ctx) => ((e1.innerHTML = ctx?.content || ''), e1);
162234

163235
const r2 = jsx(fc, { id: 2 });
164236
expect(r2 instanceof HTMLDivElement).toBeTruthy();
165237
const e2 = r2 as HTMLElement;
166238
expect(e2.outerHTML).toBe('<div id="1"></div>');
167239

168-
setContext('div', undefined);
240+
setContext('div', true);
169241
const r3 = jsx(fc, { id: 3 });
170242
expect(r3 instanceof Function).toBeTruthy();
171-
const rr3 = (r3 as TagFunc)({}, undefined);
243+
const rr3 = (r3 as ContextFunc)({ content: 'test' });
244+
expect(getContext()).not.toBe(null);
245+
clearContext();
246+
172247
expect(rr3 instanceof HTMLDivElement).toBeTruthy();
173248
const e3 = rr3 as HTMLElement;
174-
expect(e3.outerHTML).toBe('<div id="1"></div>');
175-
clearContext();
249+
expect(e3.outerHTML).toBe('<div id="1">test</div>');
176250

177251
setContext(fc, {});
178252
const r4 = jsx(fc, { id: 4 });
179253
expect(r4 instanceof HTMLDivElement).toBeTruthy();
180254
const e4 = r4 as HTMLElement;
181255
expect(e4.outerHTML).toBe('<div id="1"></div>');
182256
expect(getContext()).toBe(null);
257+
258+
setContext(fc, { content: 'test' });
259+
const r5 = jsx(fc, { id: 5 });
260+
expect(r5 instanceof HTMLDivElement).toBeTruthy();
261+
expect(getContext()).toBe(null);
262+
const e5 = r5 as HTMLElement;
263+
expect(e5.outerHTML).toBe('<div id="1">test</div>');
264+
265+
setContext(fc, undefined);
266+
const r6 = jsx(fc, { id: 5 });
267+
expect(r6 instanceof Function).toBeTruthy();
268+
const rr6 = (r6 as ContextFunc)({ content: 'test' });
269+
expect(getContext()).not.toBe(null);
270+
clearContext();
271+
expect(getContext()).toBe(null);
272+
expect(rr6 instanceof HTMLDivElement).toBeTruthy();
273+
const e6 = rr3 as HTMLElement;
274+
expect(e6.outerHTML).toBe('<div id="1">test</div>');
183275
});
184276

185277
test('Comment', () => {
@@ -205,9 +297,9 @@ describe('Test Runtime', () => {
205297
const f2 = Fragment({ children: 'fragment2' });
206298
expect(f2 instanceof Function).toBeTruthy();
207299
const rf = (f2 as TagFunc)({}, undefined);
300+
clearContext();
208301

209302
expect(rf instanceof DocumentFragment).toBeTruthy();
210303
expect((rf as DocumentFragment).firstChild?.textContent).toBe('fragment2');
211-
clearContext();
212304
});
213305
});

index.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export type JsxNode = TagFunc | Node | NodeList | string | number | boolean | null | undefined;
1+
export type JsxNode = ContextFunc | TagFunc | Node | NodeList | string | number | boolean | null | undefined;
22

33
export type JsxRef = { current?: Node };
44

@@ -9,13 +9,15 @@ export type OptionsRef = {
99
};
1010

1111
export type OptionsChildren = {
12-
children: JsxNode | JsxNode[];
12+
children?: JsxNode | JsxNode[];
1313
};
1414

1515
export type Options = Partial<OptionsAttributes & OptionsChildren & OptionsRef>;
1616

1717
export type TagFunc = (o: Options, ctx?: any) => Node | null;
1818

19+
export type ContextFunc = (ctx?: any) => Node | null;
20+
1921
export namespace JSX {
2022
export interface IntrinsicElements {
2123
// HTML
@@ -214,7 +216,7 @@ function renderChildren(fragment: DocumentFragment, children: JsxNode | JsxNode[
214216
}
215217
}
216218

217-
function render(element: HTMLElement | DocumentFragment, options: Options, ctx: any) {
219+
function render(element: Element | DocumentFragment, options: Options, ctx: any) {
218220
if (options instanceof Object) {
219221
for (const o in options) {
220222
switch (o) {
@@ -233,7 +235,7 @@ function render(element: HTMLElement | DocumentFragment, options: Options, ctx:
233235
}
234236
break;
235237
default:
236-
if (element instanceof HTMLElement) {
238+
if (element instanceof Element) {
237239
if (typeof options[o] === 'function') {
238240
(element as any)[o] = options[o];
239241
} else if (options[o] != null) {
@@ -248,16 +250,32 @@ function render(element: HTMLElement | DocumentFragment, options: Options, ctx:
248250
return element;
249251
}
250252

251-
let context: any = null;
253+
let context: { tag: keyof JSX.IntrinsicElements | TagFunc, ctx: any } | null;
252254

253255
function jsx(tag: keyof JSX.IntrinsicElements | TagFunc, options: Options) {
254-
const f = typeof tag === 'function' ? (ctx: any) => tag(options, ctx) : (ctx: any) => render(document.createElement(tag), options, ctx);
256+
const f: ContextFunc = typeof tag === 'function' ? (ctx: any) => tag(options, ctx) : (ctx: any) => {
257+
258+
if (options.xmlns != null) {
259+
if (!ctx) {
260+
throw new Error('Declaring a namespace on an element using xmlns: attribute requires context');
261+
}
262+
263+
if (ctx.namespaceURI !== options.xmlns) {
264+
ctx = { ...ctx };
265+
ctx.namespaceURI = options.xmlns;
266+
}
267+
}
268+
269+
const element = ctx?.namespaceURI == null ? document.createElement(tag) : document.createElementNS(ctx.namespaceURI, tag);
270+
271+
return render(element, options, ctx);
272+
};
255273

256274
if (!context) {
257275
return f(undefined);
258276
}
259277

260-
if (context.tag === tag) {
278+
if (context.tag === tag && context.ctx) {
261279
const result = f(context.ctx);
262280
context = null;
263281
return result;
@@ -267,7 +285,7 @@ function jsx(tag: keyof JSX.IntrinsicElements | TagFunc, options: Options) {
267285
}
268286

269287
function Fragment(options: Options) {
270-
const f = (ctx: any) => render(document.createDocumentFragment(), options, ctx);
288+
const f = (ctx: any) => render(document.createDocumentFragment(), options, ctx) as DocumentFragment;
271289

272290
if (!context) {
273291
return f(undefined);
@@ -291,6 +309,10 @@ function Comment(options: Options) {
291309
}
292310

293311
function setContext(tag: keyof JSX.IntrinsicElements | TagFunc, ctx: any) {
312+
if (ctx?.namespaceURI != null) {
313+
throw new Error('namespaceURI context property is reserved for internal use');
314+
}
315+
294316
context = { tag, ctx };
295317
}
296318

jsx-runtime.commonjs/index.d.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export type JsxNode = TagFunc | Node | NodeList | string | number | boolean | null | undefined;
1+
export type JsxNode = ContextFunc | TagFunc | Node | NodeList | string | number | boolean | null | undefined;
22
export type JsxRef = {
33
current?: Node;
44
};
@@ -9,10 +9,11 @@ export type OptionsRef = {
99
ref: JsxRef;
1010
};
1111
export type OptionsChildren = {
12-
children: JsxNode | JsxNode[];
12+
children?: JsxNode | JsxNode[];
1313
};
1414
export type Options = Partial<OptionsAttributes & OptionsChildren & OptionsRef>;
1515
export type TagFunc = (o: Options, ctx?: any) => Node | null;
16+
export type ContextFunc = (ctx?: any) => Node | null;
1617
export declare namespace JSX {
1718
interface IntrinsicElements {
1819
a: any;
@@ -192,10 +193,13 @@ export declare namespace JSX {
192193
view: any;
193194
}
194195
}
195-
declare function jsx(tag: keyof JSX.IntrinsicElements | TagFunc, options: Options): Node | ((ctx: any) => Node | null) | null;
196-
declare function Fragment(options: Options): HTMLElement | DocumentFragment | ((ctx: any) => HTMLElement | DocumentFragment);
196+
declare function jsx(tag: keyof JSX.IntrinsicElements | TagFunc, options: Options): Node | ContextFunc | null;
197+
declare function Fragment(options: Options): DocumentFragment | ((ctx: any) => DocumentFragment);
197198
declare function Comment(options: Options): Comment;
198199
declare function setContext(tag: keyof JSX.IntrinsicElements | TagFunc, ctx: any): void;
199200
declare function clearContext(): void;
200-
declare function getContext(): any;
201+
declare function getContext(): {
202+
tag: TagFunc | keyof JSX.IntrinsicElements;
203+
ctx: any;
204+
} | null;
201205
export { jsx, jsx as jsxs, Fragment, Comment, setContext, clearContext, getContext, };

0 commit comments

Comments
 (0)