Sam Holguin
Sam Holguin

Reputation: 563

Vanilla Javascript manipulating elements on window resize

I'm playing around with a Javascript tab system.

As the window width falls below a certain breakpoint the tab content inserts after the links for an accordion layout, and the process is reversed as you increase the window width.

Everything works as intended as you reduce the window size, but breaks as you increase the window width. The following code is causing the issue tabBody.appendChild( tabBodyItems[k] );.

Only 2 of the 3 .c-Tabs_BodyItem elements append???

HTML

<div class="c-Tabs" id="js-Tabs">
    <div class="o-Container">
        <div class="o-Row c-Tabs_Head">
            <div class="o-Col-sm-4 c-Tabs_HeadItem">
                <a href="" class="c-Tabs_Link">Link 1</a>
            </div>
            <div class="o-Col-sm-4 c-Tabs_HeadItem is-active">
                <a href="" class="c-Tabs_Link">Link 2</a>
            </div>
            <div class="o-Col-sm-4 c-Tabs_HeadItem">
                <a href="" class="c-Tabs_Link">Link 3</a>
            </div>
        </div>
        <div class="c-Tabs_Body">
            <div class="c-Tabs_BodyItem">
                <ul>
                    <li>Lorem ipsum dolor sit amet, consectetur adipisicing elit.</li>
                    <li>Lorem ipsum dolor sit amet, consectetur adipisicing elit.</li>
                </ul>
            </div>
            <div class="c-Tabs_BodyItem is-active">
                <ul>
                    <li>Lorem ipsum dolor sit amet, consectetur adipisicing elit.</li>
                    <li>Lorem ipsum dolor sit amet, consectetur adipisicing elit.</li>
                </ul>
            </div>
            <div class="c-Tabs_BodyItem">
                <ul>
                    <li>Lorem ipsum dolor sit amet, consectetur adipisicing elit.</li>
                    <li>Lorem ipsum dolor sit amet, consectetur adipisicing elit.</li>
                </ul>
            </div>
        </div>
    </div>
</div>

Javascript

I won't show the Javascript for the actual tabbing system; this isn't causing the issue.

function insertAfter( el, referenceNode )
{
    referenceNode.parentNode.insertBefore( el, referenceNode.nextSibling );
}

function debounce( func, wait, immediate )
{
    var timeout;

    return function() {
        var context = this, args = arguments;
        var later = function() {
            timeout = null;
            if (!immediate) func.apply(context, args);
        };
        var callNow = immediate && !timeout;
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
        if (callNow) func.apply(context, args);
    };
}

var tabHeadItems = document.getElementsByClassName( 'c-Tabs_HeadItem' );
var tabBody = document.querySelector( '.c-Tabs_Body' );
var tabBodyItems = document.getElementsByClassName( 'c-Tabs_BodyItem' );
var isTabs = true;

function setTab()
{
    if ( window.matchMedia( '(min-width: 768px)' ).matches && isTabs === false )
    {
        for( var k = 0, lenK = tabBodyItems.length; k < lenK; k++ )
        {
            tabBody.appendChild( tabBodyItems[k] );
        }

        isTabs = true;
    }
    else if ( window.matchMedia( '(max-width: 767px)' ).matches && isTabs === true )
    {
        for( var l = 0, lenL = tabBodyItems.length; l < lenL; l++ )
        {
            insertAfter( tabBodyItems[l], tabHeadItems[l] );
        }

        isTabs = false;
    }
}

setTab();

var debounceSetTab = debounce(function() {
    setTab();
}, 250 );

window.addEventListener( 'resize', debounceSetTab );

Upvotes: 4

Views: 998

Answers (1)

Barmar
Barmar

Reputation: 781721

The reason is that getElementsByClassName returns a live NodeList. This means that as the DOM changes, the content of the NodeList changes to reflect it. The order of the elements tabBodyItems is based on their positions in the DOM, so when you move the elements around in the DOM, their indexes in tabBodyItems change to reflect this. This is happening while you're looping through the collection in your for loop, so you're skipping elements because they get renumbered.

The simplest fix to this is to use document.querySelectorAll instead of document.getElementsByClassName. This returns a static NodeList.

function insertAfter( el, referenceNode )
{
    referenceNode.parentNode.insertBefore( el, referenceNode.nextSibling );
}

function debounce( func, wait, immediate )
{
    var timeout;

    return function() {
        var context = this, args = arguments;
        var later = function() {
            timeout = null;
            if (!immediate) func.apply(context, args);
        };
        var callNow = immediate && !timeout;
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
        if (callNow) func.apply(context, args);
    };
}

var tabHeadItems = document.querySelectorAll( '.c-Tabs_HeadItem' );
var tabBody = document.querySelector( '.c-Tabs_Body' );
var tabBodyItems = document.querySelectorAll( '.c-Tabs_BodyItem' );
var isTabs = true;

function setTab()
{
    if ( window.matchMedia( '(min-width: 768px)' ).matches && isTabs === false )
    {
        for( var k = 0, lenK = tabBodyItems.length; k < lenK; k++ )
        {
            tabBody.appendChild( tabBodyItems[k] );
        }

        isTabs = true;
    }
    else if ( window.matchMedia( '(max-width: 767px)' ).matches && isTabs === true )
    {
        for( var l = 0, lenL = tabBodyItems.length; l < lenL; l++ )
        {
            insertAfter( tabBodyItems[l], tabHeadItems[l] );
        }

        isTabs = false;
    }
}

setTab();

var debounceSetTab = debounce(function() {
    setTab();
}, 250 );

window.addEventListener( 'resize', debounceSetTab );
<div class="c-Tabs" id="js-Tabs">
    <div class="o-Container">
        <div class="o-Row c-Tabs_Head">
            <div class="o-Col-sm-4 c-Tabs_HeadItem">
                <a href="" class="c-Tabs_Link">Link 1</a>
            </div>
            <div class="o-Col-sm-4 c-Tabs_HeadItem is-active">
                <a href="" class="c-Tabs_Link">Link 2</a>
            </div>
            <div class="o-Col-sm-4 c-Tabs_HeadItem">
                <a href="" class="c-Tabs_Link">Link 3</a>
            </div>
        </div>
        <div class="c-Tabs_Body">
            <div class="c-Tabs_BodyItem" id="body1">
                <ul>
                    <li>Lorem ipsum dolor sit amet, consectetur adipisicing elit.</li>
                    <li>Lorem ipsum dolor sit amet, consectetur adipisicing elit.</li>
                </ul>
            </div>
            <div class="c-Tabs_BodyItem is-active" id="body2">
                <ul>
                    <li>Lorem ipsum dolor sit amet, consectetur adipisicing elit.</li>
                    <li>Lorem ipsum dolor sit amet, consectetur adipisicing elit.</li>
                </ul>
            </div>
            <div class="c-Tabs_BodyItem"id="body3">
                <ul>
                    <li>Lorem ipsum dolor sit amet, consectetur adipisicing elit.</li>
                    <li>Lorem ipsum dolor sit amet, consectetur adipisicing elit.</li>
                </ul>
            </div>
        </div>
    </div>
</div>

Upvotes: 4

Related Questions