🔍

LitElementで拡大縮小表示をやってみた

2022/12/12に公開

こんにちは、エンジニア@milab です。
前回Litのライフサイクルについて記事を書きましたが
今回は拡大表示用のWebCompomentを、LitElementベースで作ってみます。

やりたいこと

  • 属性のzoom指定で拡大/縮小を制御する
  • 拡大時は画面中央に要素を表示する
  • zoomIn(),zoomOut()メソッドを実装する
  • プロパティのattributeオプションも使いたい

サンプルソース

HTML側

<zoom-view>タグ内に拡大要素を定義する。

index.html
  <body>
    <zoom-view id="zoomView" zoom="false">
      <div class="logo">
        <img src="/lit.svg" />
        <span>Lit</span>
      </div>
    </zoom-view>
    <button
      onclick="document.getElementById('zoomView').setAttribute('zoom', 'true')"
    >
      Set attribute true!
    </button>
    <button
      onclick="document.getElementById('zoomView').setAttribute('zoom', 'false')"
    >
      Set attribute false!
    </button>
    <button onclick="document.getElementById('zoomView').zoomIn()">
      Call zoomIn()!
    </button>
    <button onclick="document.getElementById('zoomView').zoomOut()">
      Call zoomOut()!
    </button>
  </body>

TypeScript側のソース

zoom-view.element.ts
import { LitElement, css, html, PropertyValues } from 'lit';
import { customElement, property } from 'lit/decorators.js';

@customElement('zoom-view')
export class MyElement extends LitElement {
  static override styles = css`
  :host {
  }
  #box {
    position: relative;
  }
  #zoomBox{
    position: absolute;
  }
`;

  @property({
    type: Boolean,
    converter: (value) => {
      console.log(value);
      return value && value.toLowerCase() === 'true';
    },
    attribute: 'zoom',
  })
  private _zoom?: boolean = false;

  private get _box(): HTMLDivElement | null {
    return this.renderRoot?.querySelector('#box') ?? null;
  }
  private get _zoomBox(): HTMLDivElement | null {
    return this.renderRoot?.querySelector('#zoomBox') ?? null;
  }

  private _keyframes: any[] = [];

  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }

  override attributeChangedCallback(
    name: string,
    _old: string | null,
    value: string | null
  ): void {
    super.attributeChangedCallback(name, _old, value);
    this.requestUpdate(name, _old);
  }

  override render() {
    console.log('render');
    return html`<div id="box"><div id="zoomBox"><slot /></div></div>`;
  }

  override firstUpdated(changedProperties: PropertyValues): void {
    if (this._box && this._zoomBox) {
      this._box.style.height = `${this._zoomBox.offsetHeight}px`;
      this._box.style.width = `${this._zoomBox.offsetWidth}px`;
    }
    return super.firstUpdated(changedProperties);
  }

  override updated(changedProperties: PropertyValues): void {
    if (changedProperties.has('zoom') && this._zoomBox && this._box) {
      this._zoomBox.style.transformOrigin = 'top left';
      if (this._zoom) {
        this._box.style.backgroundColor = '#EEE';
        const scale = 1.5;
        const toY =
          (document.documentElement.clientHeight -
            (this._box.offsetHeight as number) * 1.5) /
          2;
        const toX =
          (document.documentElement.clientWidth -
            (this._box.offsetWidth as number) * 1.5) /
          2;
        this._keyframes = [
          {
            transform: `1`,
            left: `${this._zoomBox.offsetLeft}px`,
            top: `${this._zoomBox.offsetTop}px`,
          },
          {
            transform: `scale(${scale})`,
            left: `${toX}px`,
            top: `${toY}px`,
          },
        ];
        this._zoomBox.animate(this._keyframes, {
          iterations: 1,
          fill: 'forwards',
          duration: 300,
        });
      } else {
        this._zoomBox.animate(this._keyframes, {
          iterations: 1,
          fill: 'forwards',
          duration: 300,
          direction: 'reverse',
        });
        this._box.style.backgroundColor = '';
      }
    }
  }

  public zoomIn() {
    if (this._zoom === true) return;
    const _old = this._zoom;
    this._zoom = true;
    this.requestUpdate('zoom', _old);
  }

  public zoomOut() {
    if (this._zoom === false) return;
    const _old = this._zoom;
    this._zoom = false;
    this.requestUpdate('zoom', _old);
  }
}

Chromeで動作確認ができます。

コンソールログで確認してみる!

ボタンを押下して表示されたログは赤い四角で囲んでいます。

最初

デフォルトでは独自に定義した<zoom-view>タグの要素zoomにfalseをセットしているため
最初は「false」がコンソールに表示されます。

Set attribute true!ボタンを押下する

ボタンを押下するとsetAttribute()が実行されて
<zoom-view>タグの要素zoomにtrueがセットされます。
属性が変わったのでattributeChangedCallback()が実行されます。

changedPropertiesは、requestUpdate()メソッドの処理が終わると以下のように
trueからfalseに変わります。
changedPropertiesには変更したものだけがセットされます。

# Key Value
0 _zoom false
1 zoom false

Set attribute false!ボタンを押下する

ボタンを押下するとsetAttribute()が実行されて
<zoom-view>タグの要素zoomにfalseがセットされます。
属性が変わったのでattributeChangedCallback()が実行されます。

changedPropertiesは、requestUpdate()メソッドの処理が終わると以下のように
今度はfalseからtrueに変わります。

# Key Value
0 _zoom true
1 zoom true

Call zoomIn()!ボタンを押下する

ボタン押下によりイベントzoomIn()が呼び出されます。
requestUpdate()メソッドを使って手動でzoomにfalseをセットします。
HTML要素のzoomは変わらないので、attributeChangedCallback()は実行されません。

Call zoomOut()!ボタンを押下する

最後に

これがReactでもAngularでも使えるのは便利ですね。
ほかにもjQueryやHTMLとも、もちろん一緒に使えるので
フレームワークを導入するのは敷居が高いけれど・・・といった場合にも使えそうです!

参考文献

https://lit.dev/docs/components/properties/

https://developer.mozilla.org/ja/docs/Web/API/Element/setAttribute

Discussion