Skip to content

Commit

Permalink
dev
Browse files Browse the repository at this point in the history
  • Loading branch information
yandeu committed Dec 6, 2021
1 parent fc6d6b0 commit 6056d59
Show file tree
Hide file tree
Showing 6 changed files with 267 additions and 5 deletions.
33 changes: 32 additions & 1 deletion src/core.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import './core.types'
import { HTMLElementSSR } from './regexDom'

export const isSSR = () => typeof _nano !== 'undefined' && _nano.isSSR === true

Expand Down Expand Up @@ -146,7 +147,7 @@ export const _render = (comp: any): any => {
if (Array.isArray(comp)) return comp.map(c => _render(c)).flat()

// function
if (typeof comp === 'function') return _render(comp())
if (typeof comp === 'function' && !comp.isClass) return _render(comp())

// if component is a HTMLElement (rare case)
if (comp.component && comp.component.tagName && typeof comp.component.tagName === 'string')
Expand All @@ -161,6 +162,9 @@ export const _render = (comp: any): any => {
// object
if (typeof comp === 'object') return []

// sometimes in SSR
if (comp.isClass) return new comp().render()

console.warn('Something unexpected happened with:', comp)
}

Expand Down Expand Up @@ -201,6 +205,33 @@ const hNS = (tag: string) => document.createElementNS('http://www.w3.org/2000/sv

// https://stackoverflow.com/a/42405694/12656855
export const h = (tagNameOrComponent: any, props: any, ...children: any) => {
// render WebComponent in SSR
if (
isSSR() &&
typeof tagNameOrComponent === 'string' &&
tagNameOrComponent.includes('-') &&
_nano.customElements.has(tagNameOrComponent)
) {
const customElement = _nano.customElements.get(tagNameOrComponent)

const component = render(customElement) as HTMLElementSSR
// get the wrapping html tag and the innerText
const match = component.toString().match(/^<(?<tag>[a-z]+)>(.*)<\/\k<tag>>$/)

if (match) {
const element = new HTMLElementSSR(match[1])
element.innerText = match[2]
if (props) {
for (const [key, value] of Object.entries(props)) {
element.setAttribute(key, value as string)
}
}
return element
} else {
return 'COULD NOT RENDER WEB-COMPONENT'
}
}

// if tagNameOrComponent is a component
if (typeof tagNameOrComponent !== 'string')
return { component: tagNameOrComponent, props: { ...props, children: children } }
Expand Down
1 change: 1 addition & 0 deletions src/core.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ declare var _nano: {
document: Document
isSSR: true | undefined
location: { pathname: string }
customElements: Map<string, any>
}

declare namespace Deno {}
Expand Down
18 changes: 17 additions & 1 deletion src/customElementsMode.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,32 @@
import { h, render, _render } from './core'
import { h, isSSR, render, _render } from './core'

interface CustomElementsParameters {
mode?: 'open' | 'closed'
delegatesFocus?: boolean
}

const defineAsCustomElementsSSR = (
component: any,
componentName: string,
_publicProps: string[] = [],
_options: any = {}
) => {
if (!/^[a-zA-Z0-9]+-[a-zA-Z0-9]+$/.test(componentName))
console.log(`Error: WebComponent name "${componentName}" is invalid.`)
else _nano.customElements.set(componentName, component)
}

export const defineAsCustomElements: (
component: any,
componentName: string,
publicProps: string[],
params?: CustomElementsParameters
) => void = function (component, componentName, publicProps, { mode = 'closed', delegatesFocus = false } = {}) {
if (isSSR()) {
defineAsCustomElementsSSR(component, componentName, publicProps)
return
}

customElements.define(
componentName,
class extends HTMLElement {
Expand Down
4 changes: 2 additions & 2 deletions src/regexDom.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { escapeHtml } from './helpers'

class HTMLElementSSR {
export class HTMLElementSSR {
public tagName: string
public isSelfClosing: boolean = false
public nodeType: null | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 = null
Expand Down Expand Up @@ -138,7 +138,7 @@ export class DocumentSSR {
}

createElementNS(_URI: string, tag: string) {
return new HTMLElementSSR(tag) as unknown as HTMLElement
return this.createElement(tag)
}

createTextNode(text: string) {
Expand Down
2 changes: 1 addition & 1 deletion src/ssr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const initGlobalVar = () => {
const isSSR = detectSSR() === true ? true : undefined
const location = { pathname: '/' }
const document = isSSR ? documentSSR() : window.document
globalThis._nano = { isSSR, location, document }
globalThis._nano = { isSSR, location, document, customElements: new Map() }
}
initGlobalVar()

Expand Down
214 changes: 214 additions & 0 deletions test/nodejs/customElementsMode.ssr.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
/**
* @jest-environment node
*/

import Nano, { Component, defineAsCustomElements } from '../../lib/index.js'
import { initSSR, renderSSR } from '../../lib/ssr.js'
import { wait } from './helpers.js'

const spy = jest.spyOn(global.console, 'error')

initSSR()

test('should render as web components', async () => {
class Test extends Component {
render() {
return <div>test</div>
}
}
defineAsCustomElements(Test, 'nano-test1', [])

const html = renderSSR(
<div>
<nano-test1></nano-test1>
</div>
)

expect(html).toBe('<div><div>test</div></div>')
expect(spy).not.toHaveBeenCalled()
})

test('should render with props', async () => {
class Test extends Component {
value: string

constructor(props: { value: string }) {
super(props)

this.value = props.value
}

render() {
return <div>test : {this.value}</div>
}
}
defineAsCustomElements(Test, 'nano-test3', ['value'], { mode: 'open' })

document.body.innerHTML = '<nano-test3 value="fuga"></nano-test3>'

await wait()

const comp = document.querySelector('nano-test3')
expect(comp?.shadowRoot?.innerHTML).toEqual('<div><div>test : fuga</div></div>')
expect(spy).not.toHaveBeenCalled()
})

test('should update render result with props change', async () => {
class Test extends Component {
value: string

constructor(props: { value: string }) {
super(props)

this.value = props.value
}

render() {
return <div>test : {this.value}</div>
}
}
defineAsCustomElements(Test, 'nano-test4', ['value'], { mode: 'open' })

document.body.innerHTML = '<nano-test4 value="fuga"></nano-test4>'

await wait()

const comp = document.querySelector('nano-test4')
expect(comp?.shadowRoot?.innerHTML).toEqual('<div><div>test : fuga</div></div>')

document.body.innerHTML = '<nano-test4 value="hoge"></nano-test4>'
const compChanged = document.querySelector('nano-test4')
expect(compChanged?.shadowRoot?.innerHTML).toEqual('<div><div>test : hoge</div></div>')
expect(spy).not.toHaveBeenCalled()
})

test('should change render result with state change', async () => {
class Test extends Component {
value: number = 0

changeValue(newValue: number) {
this.value += newValue
this.update()
}

render() {
return (
<div>
<div>Counter: {this.value}</div>
<button onClick={() => this.changeValue(1)}>Increment</button>
</div>
)
}
}
defineAsCustomElements(Test, 'nano-test5', [], { mode: 'open' })

document.body.innerHTML = '<nano-test5></nano-test5>'

await wait()

const comp = document.querySelector('nano-test5')
expect(comp?.shadowRoot?.innerHTML).toEqual('<div><div><div>Counter: 0</div><button>Increment</button></div></div>')

comp?.shadowRoot?.querySelector('button')?.click()
expect(comp?.shadowRoot?.innerHTML).toEqual('<div><div><div>Counter: 1</div><button>Increment</button></div></div>')
expect(spy).not.toHaveBeenCalled()
})

test('should keep state with props change', async () => {
class Test extends Component {
stateValue: number = 0
value: number

constructor(props: { value: number }) {
super(props)
this.value = props.value
}

changeValue(newValue: number) {
this.stateValue += newValue
this.update()
}

render() {
return (
<div>
<div>Counter: {this.stateValue}</div>
<div>props: {this.value}</div>
<button onClick={() => this.changeValue(1)}>Increment</button>
</div>
)
}
}
defineAsCustomElements(Test, 'nano-test6', ['value'], { mode: 'open' })

document.body.innerHTML = '<nano-test6 value="1"></nano-test6>'

await wait()

const comp = document.querySelector('nano-test6')
expect(comp?.shadowRoot?.innerHTML).toEqual(
'<div><div><div>Counter: 0</div><div>props: 1</div><button>Increment</button></div></div>'
)

comp?.shadowRoot?.querySelector('button')?.click()
expect(comp?.shadowRoot?.innerHTML).toEqual(
'<div><div><div>Counter: 1</div><div>props: 1</div><button>Increment</button></div></div>'
)
// @ts-ignore
comp.attributes.value?.value = 2
expect(comp?.shadowRoot?.innerHTML).toEqual(
'<div><div><div>Counter: 1</div><div>props: 2</div><button>Increment</button></div></div>'
)
expect(spy).not.toHaveBeenCalled()
})

test('should render also with functional component', async () => {
const Test = function ({ value }: { value: string }) {
return <p>{value}</p>
}

defineAsCustomElements(Test, 'nano-test7', ['value'], { mode: 'open' })

document.body.innerHTML = '<nano-test7 value="hoge"></nano-test7>'

await wait()

const comp = document.querySelector('nano-test7')
expect(comp?.shadowRoot?.innerHTML).toEqual('<div><p>hoge</p></div>')
// @ts-ignore
comp.attributes.value?.value = 'bar'
expect(comp?.shadowRoot?.innerHTML).toEqual('<div><p>bar</p></div>')
expect(spy).not.toHaveBeenCalled()
})

test('should render also with slot', async () => {
class Test extends Component {
header: string
children: any

constructor(props: { header: string; children: any }) {
super(props)
this.header = props.header
this.children = props.children
}

render() {
return (
<div>
<header>{this.header}</header>
<main>{this.children}</main>
</div>
)
}
}

defineAsCustomElements(Test, 'nano-test8', ['header'], { mode: 'open' })

document.body.innerHTML = '<nano-test8 header="nano jsx"><p>hoge</p></nano-test8>'

await wait()

const comp = document.querySelector('nano-test8')
expect(comp?.shadowRoot?.innerHTML).toEqual('<div><div><header>nano jsx</header><main><p>hoge</p></main></div></div>')
expect(spy).not.toHaveBeenCalled()
})

0 comments on commit 6056d59

Please sign in to comment.