Search code examples
javascriptcssecmascript-6responsive-designmedia-queries

How to listen to Container Style Query change


Update 2 - Added a JS BIN: https://jsbin.com/wujizuyuqi/edit?html,console,output

<!doctype html>
<html lang="en" data-bs-theme="auto">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="DublinDev">

    <title>Title</title>

    <style>
        .parent {
            container-type: size;
        }

        @container (min-width: 500px) {
            .child {
                --size: "Large";
                background-color: olive;
            }
        }

        @container (max-width: 499px) {
            div .child {
                --size: "Small";
                background-color: cyan;
            }
        }
    </style>
</head>

<body>
    <div id="parent" class="parent" style="width: 100%; height: 200px; border: 1px solid #ccc;">
        <div id="child" style="width: 100%; height: 100%;" class="child"></div>
    </div>

    <script>
        const parent = document.getElementById("parent");
        const child = document.getElementById("child");

        const resizeObserver = new ResizeObserver((entries) => {
            var size = window.getComputedStyle(child).getPropertyValue("--size")
            console.log(`Size: ${size}`);
        });

        resizeObserver.observe(child);
    </script>
</body>

</html>

Note, the whole point is to try getting rid of the resize observer and rely on an event, the same way as it's done for the regular media quires.

Update 1 - I found a horrible workaround based on a custom property + ResizeObserver + window.getComputedStyle(), it might hold the water until a better solution arrives. It'd be still be great to know if the listener is coming.

Questions: How do I add a listener for a container style query change by analogy with media queries?


Solution

  • I was playing around with <iframe>, since it has its own window interface.
    What I did here:

    1. create a "dummy" iframe
    2. set the iframe size to the exact same size as the desired element
    3. get a MediaQueryList using .matchMedia() method

    It still needs a ResizeObserver (or a eventListener on resize event) to update the iframe size according to the element size, when it changes. And I don't think you can't get rid of any sort of observer until there's a native implementation like element.matchMedia();
    It's a bit of a hacky wacky solution, but you will get the same MediaQueryList as if you would run window.matchMedia() and you can work the same way with it. .matches, .onchange, .addEventListener() ..

    Here's a JSFiddle because SO doesn't like iframes. Also a note, in JSFiddle and JS Bin the eventListener doesn't work for the MediaQueryList coming from an iframe. So instad, I recommend to create a simple .html file and open it in your browser.
    https://jsfiddle.net/sca5kbgh/55/

    // HTML

    <div id="parent" class="parent" style="width: 100%; height: 200px; border: 1px solid #ccc;">
        <div id="child" style="width: 100%; height: 100%;" class="child"></div>
    </div>
    

    // CSS

    .parent {
      container-type: size;
    }
    
    @container (min-width: 500px) {
      .child {
        background-color: olive;
      }
    }
    
    @container (max-width: 499px) {
      div .child {
        background-color: cyan;
      }
    }
    
    

    // JS

    class ContainerMatchMedia {
        constructor (element, containerQueryString) {
        this.element = element;
        this.containerQueryString = containerQueryString;
        
        this.setupIframe();
        this.setupObserver();
        this.matchMedia();
      }
      
      setupIframe() {
        // Create an iframe, we use it to "fake" the element size annd apply matchmedia
        this.iframe = document.createElement('iframe')
        this.iframe.id = 'container-match-media';
    
        // some stylings to hide the iframe
        this.iframe.style.position = 'absolute';
        this.iframe.style.top = 0;
        this.iframe.style.left = 0;
        this.iframe.style.display = 'block';
        this.iframe.style.transform = 'translate(-100vw, -100vh)';
        
        // append the iframe to root element
        document.documentElement.appendChild(this.iframe);
      }
      
      setupObserver() {
        // we still need an observer to change the iframe size when the element changes
        this.resizeObserver = new ResizeObserver((entries) => {
          this.resizeIframeToElement(this.element, this.iframe);
        });
    
        this.resizeObserver.observe(this.element);  
      }
      
      resizeIframeToElement() {
        // get the elements dimensions
        var elementRect = this.element.getBoundingClientRect();
    
        // set the iframe dimensions to same as elements dimensions
        this.iframe.width = elementRect.width;
        this.iframe.height = elementRect.height;
        
        // here we see the MediaQueryList is changing when the iframe is being resized
        if (this.MediaQueryList) {
            console.log('Matches: ', this.MediaQueryList.matches);
        };
      }
      
      matchMedia() {
        /**
         * Now we have an iframe with same dimensions as the element
         * matchMedia will return a MediaQueryList
         */
        this.MediaQueryList = this.iframe.contentWindow.matchMedia(this.containerQueryString);
      }
    }
    
    
    const child = document.getElementById("child");
    const CMM = new ContainerMatchMedia(child, '(min-width: 500px)');
    
    // unfortunately, .onchange (or .addEventListener('change' ...)) doesn't work
    CMM.MediaQueryList.onchange = (event) => {
        console.log(event);
    }