 {"id":690,"date":"2023-04-10T11:09:25","date_gmt":"2023-04-10T18:09:25","guid":{"rendered":"https:\/\/www.mavice.com\/blog\/?p=690"},"modified":"2023-04-14T14:12:15","modified_gmt":"2023-04-14T21:12:15","slug":"dynamic-alignment-with-resizeobserver","status":"publish","type":"post","link":"https:\/\/www.mavice.com\/blog\/dynamic-alignment-with-resizeobserver\/","title":{"rendered":"Dynamic Alignment with ResizeObserver"},"content":{"rendered":"\n<h2>\u2764\ufe0f CSS3<\/h2>\n\n\n\n<p>We love modern web standards. CSS3 APIs like Flexbox and Grid offer robust tools for layout &#8211; adept at cleanly handling common scenarios. Still, special circumstances arise. When they do, JS APIs like ResizeObserver are available to bridge the gap.<\/p>\n\n\n\n<h2>A Card Problem<\/h2>\n\n\n\n<p>On entry to a client automotive pricing application, the user is presented with vehicle cards in a carousel.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" width=\"860\" height=\"709\" src=\"https:\/\/www.mavice.com\/blog\/wp-content\/uploads\/2023\/04\/card-carousel-1.png\" alt=\"\" class=\"wp-image-711\" srcset=\"https:\/\/www.mavice.com\/blog\/wp-content\/uploads\/2023\/04\/card-carousel-1.png 860w, https:\/\/www.mavice.com\/blog\/wp-content\/uploads\/2023\/04\/card-carousel-1-300x247.png 300w, https:\/\/www.mavice.com\/blog\/wp-content\/uploads\/2023\/04\/card-carousel-1-768x633.png 768w, https:\/\/www.mavice.com\/blog\/wp-content\/uploads\/2023\/04\/card-carousel-1-360x297.png 360w, https:\/\/www.mavice.com\/blog\/wp-content\/uploads\/2023\/04\/card-carousel-1-640x528.png 640w\" sizes=\"(max-width: 860px) 100vw, 860px\" \/><\/figure>\n\n\n\n<p>For some models, the cards are straightforward &#8211; names, pricing, images, marketing copy, disclaimers, etc. These cases more closely reflect the original design &#8211; a simple layout with alignment achieved with fixed heights, element positioning, and content restrictions.<\/p>\n\n\n\n<p>Despite such cases, the underlying card components are dynamic and complex. Driven by authored content, service data, and application logic, there exist a large number of element combinations and sizes which may display for other model lines. Cards may have seasonal notices, incentives, option toggles, and other conditionally displayed elements &#8211; all driven by content, services, and configuration across a large number of models.<\/p>\n\n\n\n<p>This complexity brings us to our problem &#8211; <strong>alignment<\/strong>. Without frequent design updates to keep pace with new features, the existing layout was stressed. Small content overflows or enablement of features would cause misalignment of adjacent card sections.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" width=\"842\" height=\"762\" src=\"https:\/\/www.mavice.com\/blog\/wp-content\/uploads\/2023\/04\/card-carousel-3.png\" alt=\"\" class=\"wp-image-713\" srcset=\"https:\/\/www.mavice.com\/blog\/wp-content\/uploads\/2023\/04\/card-carousel-3.png 842w, https:\/\/www.mavice.com\/blog\/wp-content\/uploads\/2023\/04\/card-carousel-3-300x271.png 300w, https:\/\/www.mavice.com\/blog\/wp-content\/uploads\/2023\/04\/card-carousel-3-768x695.png 768w, https:\/\/www.mavice.com\/blog\/wp-content\/uploads\/2023\/04\/card-carousel-3-360x326.png 360w, https:\/\/www.mavice.com\/blog\/wp-content\/uploads\/2023\/04\/card-carousel-3-640x579.png 640w\" sizes=\"(max-width: 842px) 100vw, 842px\" \/><\/figure>\n\n\n\n<p>As more models displayed alignment issues, it was clear a comprehensive fix was needed.<\/p>\n\n\n\n<p><a href=\"#demo\" data-type=\"internal\" data-id=\"#demo\">Skip to demo<\/a><\/p>\n\n\n\n<h2>Seeking Alignment<\/h2>\n\n\n\n<p>While redesigning the cards or adding restrictions on content authoring were possible, they would incur substantial client overhead.<\/p>\n\n\n\n<p>What about available layouts APIs? CSS Grid, Flexbox, and Tables were all considered, but were either insufficient or required the same hack to utilize. Cards would need to be split into constituent sections and distributed across a layout. This would break encapsulation for the card components and increase complexity. We were also already displaying cards in multiple layouts, including both carousels and grids.<\/p>\n\n\n\n<p>Maybe alignment was not the container&#8217;s responsibility, but the card&#8217;s. Could the card flexibly align itself with neighbors? With pixel-perfect alignment and no restrictions on configuration or content? Say hello to&#8230;<\/p>\n\n\n\n<h2>Resize Observer<\/h2>\n\n\n\n<p>ResizeObserver is an interface for monitoring dimension changes in the DOM. As a lower-level JS module, it&#8217;s not typically used for layout. However, with the power to efficiently monitor component sizes in realtime, it is worth considering in a pinch.<\/p>\n\n\n\n<p>So how does it work?<\/p>\n\n\n\n<pre title=\"\" class=\"wp-block-code\"><code lang=\"javascript\" class=\"language-javascript\"><code>const myObserver = new ResizeObserver(entries =&gt; {<\/code>\n    <code>entries.forEach(entry =&gt; {<\/code>\n        <code>console.log('height', entry.contentRect.height);<\/code>\n    <code>});<\/code>\n<code>});<\/code>\n\n<code><mark>const myEl = document.querySelector('.my-element');<\/mark> <mark>myObserver.observe(myEl);<\/mark><\/code><\/code><\/pre>\n\n\n\n<p>If we can measure the height of card sections, we can respond to size changes and inject a padding element to bring all section heights to the max sibling height. What might this look like in practice?<\/p>\n\n\n\n<p>This wrapper element has a dynamically injected spacer to align with its neighbors.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" width=\"573\" height=\"116\" src=\"https:\/\/www.mavice.com\/blog\/wp-content\/uploads\/2023\/04\/height-element.png\" alt=\"\" class=\"wp-image-722\" srcset=\"https:\/\/www.mavice.com\/blog\/wp-content\/uploads\/2023\/04\/height-element.png 573w, https:\/\/www.mavice.com\/blog\/wp-content\/uploads\/2023\/04\/height-element-300x61.png 300w, https:\/\/www.mavice.com\/blog\/wp-content\/uploads\/2023\/04\/height-element-360x73.png 360w\" sizes=\"(max-width: 573px) 100vw, 573px\" \/><\/figure>\n\n\n\n<p>An example with spacer elements highlighted.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" width=\"830\" height=\"713\" src=\"https:\/\/www.mavice.com\/blog\/wp-content\/uploads\/2023\/04\/els-highlighted-1.png\" alt=\"\" class=\"wp-image-724\" srcset=\"https:\/\/www.mavice.com\/blog\/wp-content\/uploads\/2023\/04\/els-highlighted-1.png 830w, https:\/\/www.mavice.com\/blog\/wp-content\/uploads\/2023\/04\/els-highlighted-1-300x258.png 300w, https:\/\/www.mavice.com\/blog\/wp-content\/uploads\/2023\/04\/els-highlighted-1-768x660.png 768w, https:\/\/www.mavice.com\/blog\/wp-content\/uploads\/2023\/04\/els-highlighted-1-360x309.png 360w, https:\/\/www.mavice.com\/blog\/wp-content\/uploads\/2023\/04\/els-highlighted-1-640x550.png 640w\" sizes=\"(max-width: 830px) 100vw, 830px\" \/><\/figure>\n\n\n\n<h2>Vue Directive<\/h2>\n\n\n\n<p>Our application is written in Vue, so we could implement a solution using a Vue directive. The logic for element observation, height calculation, and padding element injection would reside in one location and be cleanly applicable with a directive tag. When used, the directive would inject padding elements and register itself for height updates.<\/p>\n\n\n\n<p>(demo to follow)<\/p>\n\n\n\n<pre title=\"height-sync.js\" class=\"wp-block-code\"><code lang=\"javascript\" class=\"language-javascript line-numbers\">const registry = {\n    _lookup: new <strong><em>Map<\/em><\/strong>(),\n};\n\n\/\/ On resize events, trigger recalculation\nconst resizeObserver = new ResizeObserver((entries) =&gt; {\n    const targetKeys = new <strong><em>Set<\/em><\/strong>(\n        entries.map((entry) =&gt; registry._lookup.get(entry.target))\n    );\n    targetKeys.forEach((groupKey) =&gt; registry[groupKey]?.recalc());\n});\n\nfunction recalcHeights(groupKey) {\n    const els = registry[groupKey]?.els;\n    \/\/ Calculate max height and update spacers\n    ...\n    \/\/ Update spacers\n    ...\n}\n\nfunction zeroSpacers(groupKey) { ... }\n\nfunction augmentAndRegister(el, groupKey = '_') {\n    \/\/ Append spacer el\n    ...\n    \/\/ Add to registry\n    ...\n    \/\/ Observe with resizeObserver\n    resizeObserver.observe(el);\n}\n\nfunction unregister(el, groupKey = '_') { ... }\n\nfunction bind(el, { value: groupKey }) {\n    augmentAndRegister(el, groupKey);\n}\n\nfunction unbind(el, { value: groupKey }) { ... }\n\nconst <strong><em>directive <\/em><\/strong>= {\n    bind,\n    unbind,\n};\n\nexport default <strong><em>directive<\/em><\/strong>;<\/code><\/pre>\n\n\n\n<p>This allowed card sections to be tagged with a height-sync directive and unique key. The following card template code indicates the &#8216;.marketing-message-wrapper&#8217; section will be height synchronized across any card instances. (If multiple card sets are desired, a composite key with the card group may be used.)<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" width=\"483\" height=\"231\" src=\"https:\/\/www.mavice.com\/blog\/wp-content\/uploads\/2023\/04\/height-sync-dir.png\" alt=\"\" class=\"wp-image-725\" srcset=\"https:\/\/www.mavice.com\/blog\/wp-content\/uploads\/2023\/04\/height-sync-dir.png 483w, https:\/\/www.mavice.com\/blog\/wp-content\/uploads\/2023\/04\/height-sync-dir-300x143.png 300w, https:\/\/www.mavice.com\/blog\/wp-content\/uploads\/2023\/04\/height-sync-dir-360x172.png 360w\" sizes=\"(max-width: 483px) 100vw, 483px\" \/><\/figure>\n\n\n\n<h2 id=\"demo\">Alignment Demo<\/h2>\n\n\n\n<p>See this pattern implemented in vanilla JS on Codepen.<\/p>\n\n\n\n<p class=\"codepen\" data-height=\"550\" data-theme-id=\"dark\" data-default-tab=\"result\" data-slug-hash=\"xxjymeb\" data-editable=\"true\" data-user=\"TheRyjo\" style=\"height: 550px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;\">\n  <span>See the Pen <a href=\"https:\/\/codepen.io\/TheRyjo\/pen\/xxjymeb\">\n  Resize Observer &#8211; Height Demo<\/a> by Ryan Jones (<a href=\"https:\/\/codepen.io\/TheRyjo\">@TheRyjo<\/a>)\n  on <a href=\"https:\/\/codepen.io\">CodePen<\/a>.<\/span>\n<\/p>\n<script async=\"\" src=\"https:\/\/cpwebassets.codepen.io\/assets\/embed\/ei.js\"><\/script>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>\u2764\ufe0f CSS3 We love modern web standards. CSS3 APIs like Flexbox and Grid offer robust tools for layout &#8211; adept at cleanly handling common scenarios. Still, special circumstances arise. When they do, JS APIs like ResizeObserver are available to bridge the gap. A Card Problem On entry to a client&#8230;<\/p>\n","protected":false},"author":10,"featured_media":772,"comment_status":"closed","ping_status":"","sticky":false,"template":"","format":"standard","meta":[],"categories":[39],"tags":[54,51,53,52,41],"_links":{"self":[{"href":"https:\/\/www.mavice.com\/blog\/wp-json\/wp\/v2\/posts\/690"}],"collection":[{"href":"https:\/\/www.mavice.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.mavice.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.mavice.com\/blog\/wp-json\/wp\/v2\/users\/10"}],"replies":[{"embeddable":true,"href":"https:\/\/www.mavice.com\/blog\/wp-json\/wp\/v2\/comments?post=690"}],"version-history":[{"count":52,"href":"https:\/\/www.mavice.com\/blog\/wp-json\/wp\/v2\/posts\/690\/revisions"}],"predecessor-version":[{"id":767,"href":"https:\/\/www.mavice.com\/blog\/wp-json\/wp\/v2\/posts\/690\/revisions\/767"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.mavice.com\/blog\/wp-json\/wp\/v2\/media\/772"}],"wp:attachment":[{"href":"https:\/\/www.mavice.com\/blog\/wp-json\/wp\/v2\/media?parent=690"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.mavice.com\/blog\/wp-json\/wp\/v2\/categories?post=690"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.mavice.com\/blog\/wp-json\/wp\/v2\/tags?post=690"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}