WebComponents

続・JSフレームワークの末端がWebComponentsになるのか、なれるのか、検証してみた

https://qiita.com/mizchi/items/053f5b42a6d0902e9412

やりたかったこと: Reactから子に関数参照を渡すのを綺麗に実装したい

前回からの変化

  • lit-extended を使えば、テンプレートにコールバックを渡せることを知った
  • 子に対して関数参照を渡すのではなく、 CustomEvent を生成するようにして dispatchEvent するようにした
  • 一旦変換用のイベント定義辞書を渡すようにした

WebComponent定義

/* @flow */
import { html, render } from 'lit-html/lib/lit-extended'

type Props = {
  onClick?: Function,
  text: string
}

const template = (props: Props) => {
  return html`
    <button on-click=${props.onClick}>${props.text}</button>
  `
}

export default class MyButton extends HTMLElement {
  static get observedAttributes(): string[] {
    return ['text']
  }

  connectedCallback() {
    this.render()
  }

  attributeChangedCallback(_attrName: string, _old: any, _new: any) {
    this.render()
  }

  render() {
    const text = this.getAttribute('text') || 'button'
    const onClick = () =>
      this.dispatchEvent(
        new CustomEvent('my-button-clicked', {
          detail: {
            text
          }
        })
      )
    render(template({ text, onClick }), this)
  }
}

window.customElements.define('my-button', MyButton)

静的な値だけを一旦渡すようにした。

React側

reactify という関数で、特定のエレメントをReactで表示する際にどう値を渡すか定義する

/* @flow */
import ReactDOM from 'react-dom'
import * as React from 'react'
import '~/elements/MyButton'

const reactify = (name: string, opts = {}) =>
  class extends React.PureComponent {
    constructor() {
      super()
      this._listeners = []
    }
    componentDidMount() {
      const el = ReactDOM.findDOMNode(this)
      for (const eventName of Object.keys(opts.eventMap || {})) {
        const translated = opts.eventMap[eventName]
        const callback = this.props[translated]
        console.log('register', eventName, translated)
        el.addEventListener(eventName, callback)
        this._listeners.push({ eventName, callback })
      }
    }
    componentWillUnmount() {
      this._listeners.forEach(listener => {
        el.removeEventListener(listener.eventName, listener.callback)
      })
      this._listeners = []
    }
    render() {
      return React.createElement(name, this.props)
    }
  }

const MyButtonReact = reactify('my-button', {
  eventMap: {
    'my-button-clicked': 'onMyButtonClicked'
  }
})

export default function Home() {
  return (
    <div>
      <MyButtonReact
        text="my button on react"
        onMyButtonClicked={_ev => console.log('clicked!')}
      />
      {/* <my-button text="my-button" /> */}
    </div>
  )
}

「このイベントが来たらこのコールバックに渡す」という中間層を用意したことで、とりあえず動くようになった。

残ってる点

  • 本当は define した名前ではなく オブジェクト参照を使いたいが React.createElement が対応していないので、一旦こうなった。
  • やっぱ eventMap ダサい…