ResizeObserver による変更検知と Element Query
Intro
ResizeObserver の ship が進みつつある。
この仕様の解説および、ElementQuery / ContainerQuery について解説する。
ResizeObserver
ResizeObserver は、最近増えつつある ObserverFamily の 1 つであり、要素のリサイズを検知するインタフェースである。
リサイズを検知したい要素をターゲットに observe()
すると、ターゲットと矩形情報が取得できる。
const resizeObserver = new ResizeObserver((entries) => {
entries.forEach(({target, contentRect}) => {
console.log(target)
const {x, y, width, height, top, right, bottom, left} = contentRect
console.log(x)
console.log(y)
console.log(width)
console.log(height)
console.log(top)
console.log(right)
console.log(bottom)
console.log(left)
})
})
resizeObserver.observe(element) // 検知開始
resizeObserver.unobserve(element) // 検知終了
resizeObserver.disconnect(element) // 解放
基本的な Observer のインタフェースのため、使い方もそこまで難しくない。
onresize
レスポンシブ要件を満たす上で、window のサイズが変更されたことを取得するため、resize
イベントが使われた。
これは window のみに発火するため、window は不変のまま子要素の変更だけを取ることができなかった。
そこで導入されたのが ResizeObserver であったわけだが、ではなぜ resize
イベントを子要素に適用しなかったか。
resize
イベントは、そもそも view-port に対して定義されており、さらに 変更したこと だけを伝える仕様になっている。
12.1. Resizing viewports | CSSOM View Module
つまり、resize された結果を取得するためには、target
を辿りサイズを取得する必要が出る。
window.addEventListener('resize', (e) => {
const width = e.target.outerWidth
const height = e.target.outerHeight
console.log({width, height})
})
また、これを子要素で適用した場合は、scrollTop
, offset
, getBoundingClientRect()
などを用いることになるだろう。
これらは同期計算のため、Forced Synchronous Layout を引き起こし、要素のリサイズ処理がガタつくことになってしまう。
Observer を定義することにより、こうした処理を行わずに変更情報のセットを取得できるため、パフォーマンス上の問題を解決できる。
こうしたコンセプトは、IntersectionObserver が定義されたモチベーションと同じだと考えて良いだろう。
resize event polyfill
仮に、Observer のインタフェースを Event 側に寄せたいというのであれば、以下のように CustomEvent を定義することもできるだろう。
必要に応じて Passive Event Listener を検討する必要がある。
const $target = document.querySelector('textarea')
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const resize = new CustomEvent('resize', { detail: entry });
$target.dispatchEvent(resize)
}
})
resizeObserver.observe($target)
$target.addEventListener('resize', ({detail: entries}) => {
console.log(entries.contentRect)
}, {passive: true})
Element Query
ElementQuery は MediaQuery の要素版といったコンセプトで作られたライブラリである。
例えば以下のように、min-width
を境に色を変えるといったことが可能になる。
@element .minwidthpixels and (min-width: 500px) {
.minwidthpixels {
background: gold;
}
}
この程度単純な用途であれば ResizeObserver のハンドラ内で CSS の class を toggle するくらいでも実現できる。
しかし、EQ はあくまでスタイル定義を CSS 側で完結させるためのライブラリであるため、Media Query に習い Custom at-rule (@element
) を定義している。
これを実現するため、実装は以下のようなことを行なっている。
- CSS の独自拡張
- CSS パース
- 要素の変更検知
最後の変更検知は、ライブラリ内では Throttling 付きの setInterval()
で行なっているため、ResizeObserver を用いた実装でかなり効率化できるだろう。
CSS に持ってくるには、Houdini で策定中の CSS Parser API と CSS Typed OM Level 1 あたりが実装されるとネイティブで @lement
を Custom At-Rules として実装できるようになるだろう。
Container Query
Container Query は Element Query と似ているが、文字通り対象を親要素に置いている。
例えば、親要素のサイズに応じて子要素のレイアウトを変えたい場合は、Element Query のスコープで以下のように定義ができる。
@element '#sidebar' and (max-width: 300px) {
#sidebar .widget {
font-size: 10pt;
}
}
一方これを Pseudo Element で定義する提案もある。
.element:container(width >= 100px) {
/* If its container is at least 100px wide */
}
.element:container(height > 100px < 200px) {
/* If its container is between 100px and 200px high */
}
.element:container(text-align = right) {
/* If its container has a right text-align */
}
擬似要素で行う場合にも、同じように Houdini の API が揃うと、別途自前でパースする必要もなく実装が可能になるだろう。
いずれも提案自体はかなり前からあるが、実装の改善がかのうになりそうだ。
Outro
ResizeObserver によって、単一要素のリサイズがとれるようになり、それを起点にしたよりレスポンシブなレイアウトが可能となった。
ここに Parser API や Custom At-Rule, Custom Pseudo Element が実装可能になれば、ライブラリとしての Element Query 実装もかなり改善されるだろう。
より広範囲に渡るレイアウトについては、将来的には Layout API によって Worklet に落とした実装が可能なるかもしれないが、部分的な用途ではこうした方法も選択肢に入りそうだ。