Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(widget-element): add widget element #2

Merged
merged 3 commits into from
Jul 20, 2024
Merged
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
4 changes: 4 additions & 0 deletions .size-limit.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,9 @@
{
"path": "packages/scoped-styles/dist/index.js",
"limit": "400 B"
},
{
"path": "packages/widget-element/dist/index.js",
"limit": "850 B"
}
]
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Packages

- [@rambler-tech/scoped-styles](packages/scoped-styles)
- [@rambler-tech/widget-element](packages/widget-element)

## Contributing

Expand Down
15 changes: 15 additions & 0 deletions packages/widget-element/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Widget Element

Custom Element that helps you to create widgets.

## Install

```
npm install -D @rambler-tech/widget-element
```

or

```
yarn add -D @rambler-tech/widget-element
```
79 changes: 79 additions & 0 deletions packages/widget-element/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import {WidgetElement} from '.'

class TestWidget extends WidgetElement {
ready = false
changed = false
destroyed = false

static get observedAttributes() {
return ['test']
}

initialize(_shadowRoot: ShadowRoot) {
this.ready = true
}

attributeChanged() {
this.changed = true
}

destroy() {
this.destroyed = true
}
}

TestWidget.register('test-widget')

test('widget is ready', async () => {
const widget = document.createElement('test-widget') as TestWidget
const onReady = jest.fn()

widget.addEventListener('ready', onReady)

expect(widget.ready).toBe(false)

document.body.append(widget)

await Promise.resolve()

expect(widget.ready).toBe(true)
expect(onReady).toHaveBeenCalledTimes(1)
})

test('widget attribute is changed', async () => {
const widget = document.createElement('test-widget') as TestWidget

widget.setAttribute('test', '123')

expect(widget.changed).toBe(false)
expect(widget.params).toEqual({test: '123', provider: widget})

document.body.append(widget)
widget.setAttribute('test', '456')

await Promise.resolve()

expect(widget.changed).toBe(true)
expect(widget.params).toEqual({test: '456', provider: widget})
})

test('widget is destroyed', async () => {
const widget = document.createElement('test-widget') as TestWidget
const onDestroy = jest.fn()

widget.addEventListener('destroy', onDestroy)

document.body.append(widget)

await Promise.resolve()

expect(widget.destroyed).toBe(false)
expect(onDestroy).toHaveBeenCalledTimes(0)

document.body.removeChild(widget)

await Promise.resolve()

expect(widget.destroyed).toBe(true)
expect(onDestroy).toHaveBeenCalledTimes(1)
})
127 changes: 127 additions & 0 deletions packages/widget-element/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/* eslint-disable import/no-unused-modules */

/**
* Custom Element that helps you to create widgets.
*
* Define a widget
* ```ts
* import {WidgetElement} from '@rambler-tech/widget-element'
* import {createApp} from './app'
*
* class CustomWidget extends WidgetElement {
* static get observedAttributes() {
* return ['appId']
* }
*
* async initialize(shadowRoot: ShadowRoot) {
* const {appId} = this.params
* this.app = createApp(shadowRoot)
* await this.app.render({appId})
* }
*
* destroy() {
* this.app.destroy()
* }
* }
*
* CustomWidget.register('custom-widget')
* ```
*
* Use a widget
* ```tsx
* import React from 'react'
*
* export function Page() {
* const widgetRef = useRef()
*
* useEffect(() => {
* const onReady = () => {
* // Widget is ready
* }
* widgetRef.current.addEventListener('ready', onReady)
* return () => {
* widgetRef.current.removeEventListener('ready', onReady)
* }
* }, [])
*
* return (
* <div className="page">
* <h1>Hello World</h1>
* <custom-widget appid="1234" ref={widgetRef} />
* </div>
* )
* }
* ```
*/
export class WidgetElement extends HTMLElement {
#fallback!: HTMLSlotElement
#shadowRoot?: ShadowRoot

/** Register a widget custom element */
static register(tagName: string) {
customElements.define(tagName, this)
}

/** Widget params (attributes map) */
get params() {
const params: Record<string, any> = {
provider: this
}

for (let i = 0; i < this.attributes.length; i++) {
const node = this.attributes.item(i)

if (node != null) {
params[node.nodeName] = node.nodeValue
}
}

return params
}

async connectedCallback() {
this.#fallback = document.createElement('slot')
this.#shadowRoot = this.attachShadow({mode: 'closed'})

await this.initialize(this.#shadowRoot)
this.emit('ready')
}

attributeChangedCallback() {
if (this.#shadowRoot) {
this.attributeChanged()
}
}

disconnectedCallback() {
this.destroy()
this.emit('destroy')
}

/** Widget is initialized, and shadow root is attached */
// eslint-disable-next-line no-empty-function
initialize(_shadowRoot: ShadowRoot) {}

/** An attribute of widget is changed */
// eslint-disable-next-line no-empty-function
attributeChanged() {}

/** Widget is destroyed */
// eslint-disable-next-line no-empty-function
destroy() {}

/** Show fallback (slot element) */
showFallback() {
this.#shadowRoot?.appendChild(this.#fallback)
}

/** Hide fallback (slot element) */
hideFallback() {
this.#shadowRoot?.removeChild(this.#fallback)
}

/** Dispatch event */
emit(eventType: string, detail?: Record<string, any>) {
this.dispatchEvent(new CustomEvent(eventType, {detail}))
}
}
12 changes: 12 additions & 0 deletions packages/widget-element/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "@rambler-tech/widget-element",
"version": "0.0.0",
"main": "dist",
"module": "dist",
"types": "dist/index.d.ts",
"license": "MIT",
"sideEffects": false,
"publishConfig": {
"access": "public"
}
}
1 change: 1 addition & 0 deletions packages/widget-element/tsconfig.json
1 change: 1 addition & 0 deletions packages/widget-element/typedoc.json
Loading