Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions packages/vbi-component/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,20 @@
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json"
},
"devDependencies": {
"@open-wc/testing": "4.0.0",
"@rslib/core": "0.20.3",
"@rstest/core": "0.8.3",
"@types/node": "24.10.1",
"@visactor/vchart": "2.0.23-alpha.6",
"jsdom": "29.1.1",
"typescript": "6.0.2"
},
"peerDependencies": {
"@visactor/vchart": "2.0.23-alpha.6"
},
"dependencies": {
"@visactor/vseed": "workspace:*",
"@visactor/vtable": "1.23.1",
"lit": "3.3.3"
}
}
6 changes: 6 additions & 0 deletions packages/vbi-component/rslib.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { defineConfig } from '@rslib/core'
import pkg from './package.json'

export default defineConfig({
lib: [
Expand All @@ -12,6 +13,11 @@ export default defineConfig({
syntax: ['node 18'],
},
],
source: {
define: {
__VBI_COMPONENT_VERSION__: JSON.stringify(pkg.version),
},
},
output: {
sourceMap: true,
},
Expand Down
8 changes: 7 additions & 1 deletion packages/vbi-component/rstest.config.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import { defineConfig } from '@rstest/core'
import pkg from './package.json'

export default defineConfig({
globals: true,
testEnvironment: 'node',
testEnvironment: 'jsdom',
pool: 'forks',
include: ['tests/**/*.test.ts'],
exclude: ['node_modules/**', 'dist/**', '**/*.d.ts'],
includeSource: ['src/**/*.{js,ts}'],
coverage: {
enabled: false,
},
source: {
define: {
__VBI_COMPONENT_VERSION__: JSON.stringify(pkg.version),
},
},
resolve: {
alias: {
'@visactor/vbi-component': ['./src/index.ts'],
Expand Down
2 changes: 2 additions & 0 deletions packages/vbi-component/src/env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/** Injected by rslib `source.define` from package.json version */
declare const __VBI_COMPONENT_VERSION__: string
2 changes: 1 addition & 1 deletion packages/vbi-component/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const createVBIComponentPlaceholder = () => 'vbi-component'
export { VBIChartRender } from './vbi-chart-render'
62 changes: 62 additions & 0 deletions packages/vbi-component/src/shared/element.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { LitElement } from 'lit'

const VERSION = __VBI_COMPONENT_VERSION__
const CONFIG_KEY = '__vbiComponent_disableRegistryWarning__'

const warn = (message: unknown, componentInstance?: VdashElement): void => {
console.warn(message, componentInstance)
}

const error = (message: unknown, componentInstance?: VdashElement): void => {
console.error(message, componentInstance)
}

export class VdashElement extends LitElement {
get version(): string {
return VERSION
}

warn(message: unknown): void {
warn(message, this)
}

error(message: unknown): void {
error(message, this)
}
}

type CustomElementClass = Omit<typeof HTMLElement, 'new'>

/**
* Own implementation of Lit's customElement decorator.
*/
export const customElement = (tagName: string) => {
return (classOrTarget: CustomElementClass) => {
if (typeof customElements === 'undefined') return
const customElementClass = customElements.get(tagName)

if (!customElementClass) {
customElements.define(tagName, classOrTarget as CustomElementConstructor)
return
}

if (CONFIG_KEY in window) {
return
}

const el = document.createElement(tagName)
const anotherVersion = (el as VdashElement)?.version
let message = ''

if (!anotherVersion) {
message += 'is already registered by an unknown custom element handler class.'
} else if (anotherVersion !== VERSION) {
message += 'is already registered by a different version of Vdash. '
message += `This version is "${VERSION}", while the other one is "${anotherVersion}".`
} else {
message += `is already registered by the same version of Vdash (${VERSION}).`
}

warn(`The custom element "${tagName}" ${message}\nTo suppress this warning, set window.${CONFIG_KEY} to true`)
}
}
13 changes: 13 additions & 0 deletions packages/vbi-component/src/shared/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { css } from 'lit'

export const defaultStyles = css`
:host([hidden]) {
display: none;
}

:host([disabled]),
:host(:disabled) {
cursor: not-allowed;
pointer-events: none;
}
`
1 change: 1 addition & 0 deletions packages/vbi-component/src/vbi-chart-render/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { VBIChartRender } from './vbi-chart-render'
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { css, type CSSResultGroup } from 'lit'
import { defaultStyles } from 'src/shared/styles'

const styles: CSSResultGroup = [
defaultStyles,
css`
:host {
display: block;
}

.vbi-chart-render__container {
height: 100%;
width: 100%;
}
`,
]

export default styles
70 changes: 70 additions & 0 deletions packages/vbi-component/src/vbi-chart-render/vbi-chart-render.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { ISpec } from '@visactor/vchart'
import VChart from '@visactor/vchart'
import { html, type PropertyValues } from 'lit'
import { property } from 'lit/decorators.js'
import { createRef, ref } from 'lit/directives/ref.js'
import { customElement, VdashElement } from 'src/shared/element'
import styles from './vbi-chart-render.style'

type VBIChartRenderCleanup = (() => void) | undefined

/**
* Chart container for rendering VChart specifications.
*
* @tag vbi-chart-render
*
* @prop {ISpec | undefined} spec - VChart specification rendered inside the chart container.
*/
@customElement('vbi-chart-render')
export class VBIChartRender extends VdashElement {
static override get styles() {
return styles
}

@property({ attribute: false }) accessor spec: ISpec | undefined = undefined
private readonly chartContainerRef = createRef<HTMLDivElement>()
private cleanup: VBIChartRenderCleanup = undefined

override disconnectedCallback(): void {
this.cleanupRender()
super.disconnectedCallback()
}

protected override updated(changedProperties: PropertyValues<this>): void {
if (!changedProperties.has('spec')) {
return
}
this.renderChart()
}

private cleanupRender(): void {
this.cleanup?.()
this.cleanup = undefined
}

private renderChart(): void {
this.cleanupRender()
const container = this.chartContainerRef.value
if (!container || !this.spec) {
return
}

try {
const vchart = new VChart(this.spec, { dom: container })
vchart.renderSync()
this.cleanup = () => vchart.release()
} catch (error) {
this.error(`VBI chart render error: ${String(error)}`)
}
}

override render() {
return html`<div class="vbi-chart-render__container" ${ref(this.chartContainerRef)}></div>`
}
}

declare global {
interface HTMLElementTagNameMap {
'vbi-chart-render': VBIChartRender
}
}
8 changes: 0 additions & 8 deletions packages/vbi-component/tests/smoke.test.ts

This file was deleted.

111 changes: 111 additions & 0 deletions packages/vbi-component/tests/vbi-chart-render/vbi-chart-render.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import type { ISpec } from '@visactor/vchart'
import { afterEach, beforeEach, describe, expect, rs, test } from '@rstest/core'
import { elementUpdated, fixture, fixtureCleanup, html } from '@open-wc/testing'
import { VBIChartRender } from '@visactor/vbi-component'

type VChartMockInstance = {
options: { dom: HTMLElement }
release: ReturnType<typeof rs.fn>
renderSync: ReturnType<typeof rs.fn>
spec: ISpec
}

type VChartMockState = {
constructor: ReturnType<typeof rs.fn>
instances: VChartMockInstance[]
}

rs.mock('@visactor/vchart', () => {
const state: VChartMockState = {
constructor: rs.fn((spec: ISpec, options: { dom: HTMLElement }) => {
const instance: VChartMockInstance = {
options,
release: rs.fn(),
renderSync: rs.fn(),
spec,
}
state.instances.push(instance)
return instance
}),
instances: [],
}

return {
default: state.constructor,
__vchartMockState: state,
}
})

const getVChartMockState = async () => {
const module = (await import('@visactor/vchart')) as unknown as { __vchartMockState: VChartMockState }
return module.__vchartMockState
}

const createSpec = (type: string): ISpec =>
({
data: [{ id: 'source', values: [{ category: 'A', value: 1 }] }],
type,
xField: 'category',
yField: 'value',
}) as unknown as ISpec

describe('vbi-chart-render', () => {
beforeEach(async () => {
const state = await getVChartMockState()
state.instances.length = 0
rs.clearAllMocks()
})

afterEach(() => {
fixtureCleanup()
})

test('registers the custom element', () => {
expect(customElements.get('vbi-chart-render')).toBe(VBIChartRender)
})

test('renders chart container when spec is missing', async () => {
const element = await fixture<VBIChartRender>(html`<vbi-chart-render></vbi-chart-render>`)
const state = await getVChartMockState()

expect(element.shadowRoot?.querySelector('.vbi-chart-render__container')).toBeInstanceOf(HTMLElement)
expect(state.constructor).not.toHaveBeenCalled()
})

test('renders VChart into the chart container when spec is provided', async () => {
const spec = createSpec('bar')
const element = await fixture<VBIChartRender>(html`<vbi-chart-render .spec=${spec}></vbi-chart-render>`)
const state = await getVChartMockState()
const container = element.shadowRoot?.querySelector('.vbi-chart-render__container')

expect(container).toBeInstanceOf(HTMLElement)
expect(state.constructor).toHaveBeenCalledWith(spec, { dom: container })
expect(state.instances).toHaveLength(1)
expect(state.instances[0].renderSync).toHaveBeenCalledTimes(1)
})

test('releases the previous VChart instance before rendering a new spec', async () => {
const element = await fixture<VBIChartRender>(
html`<vbi-chart-render .spec=${createSpec('bar')}></vbi-chart-render>`,
)
const state = await getVChartMockState()
const firstInstance = state.instances[0]

element.spec = createSpec('line')
await elementUpdated(element)

expect(firstInstance.release).toHaveBeenCalledTimes(1)
expect(state.instances).toHaveLength(2)
expect(state.instances[1].renderSync).toHaveBeenCalledTimes(1)
})

test('releases the VChart instance when disconnected', async () => {
await fixture<VBIChartRender>(html`<vbi-chart-render .spec=${createSpec('bar')}></vbi-chart-render>`)
const state = await getVChartMockState()
const instance = state.instances[0]

fixtureCleanup()

expect(instance.release).toHaveBeenCalledTimes(1)
})
})
2 changes: 1 addition & 1 deletion packages/vbi-component/tsconfig.test.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
"src/*": ["./src/*"]
}
},
"include": ["src", "tests", "rstest.config.ts"]
"include": ["src", "tests", "rstest.config.ts", "package.json"]
}
Loading
Loading