JS to unwrap children - Need to imitate display:contents;


#1

Hi,
How can I run a script based on screen width or with a media query?

I am wanting to use flexbox and I've got a situation where I need to remove the parent wrapping div and retain the child divs in the page.

This is easily done with display:contents; but browser support is not good at this time. It is only supported in Firefox, and Chrome is in development. No support for mobiles though, which is what this is really all about.

Both of the following examples work in Firefox 56+ using display:contents; and the script is in there for other browsers.

JS Unwrap test
Page Layout Example

Flexbox is not capable of stacking divs as a sidebar without nesting them in a parent div ( .sidebar ). Not without setting a fixed height that is. As seen below...

The reason for removing the parent div .sidebar is so I can stack and reorder the display of divs for mobiles. That's where flexbox is needed, but the child divs need to break out of the .sidebar parent div to do that. The 'Page Layout Example' shows that happening in FF when @media all and (max-width: 600px) kicks in, but requires clicking the 'Run JS' button in Chrome to remove .sidebar in the test pages.

Even if the script failed to load for mobiles I would still get a usable page, just not displayed in the order that I would like.

So my questions are...

  1. Is there a way to run the script for mobiles based on screen width or with a media query?
  2. Is the script well written or is there a better way of writing it?

I found the script on JS Bin, I'm not well-versed with JS.
Thanks in advance for advice on this!

function unwrap(sidebar) {
	// place childNodes in document fragment
	var docFrag = document.createDocumentFragment();
	while (sidebar.firstChild) {
		var child = sidebar.removeChild(sidebar.firstChild);
		docFrag.appendChild(child);
	}

	// replace sidebar with document fragment
	sidebar.parentNode.replaceChild(docFrag, sidebar);
}

// Try it:
//unwrap(document.querySelector('.sidebar'));

document.getElementById('toggle').addEventListener('click', function() {
    unwrap(document.querySelector('.sidebar'));
});

This is a follow up to all that was discussed in this other thread.
Can Flexbox do this 2-Column Setup


#2

I'll let the experts answer the question properly but I have used matchMedia which is like media queries but in JS.

I use it here to add a click menu for smaller screen widths.

I'm sure the others here will have a better answer though :slight_smile:


#3

Yep, that's the concept I'm after. :slight_smile:

Maybe it could be merged into the existing unwrap script somehow. And possibly clean up the unwrap script if necessary.

With any luck we can come up with a usable substitute for display:contents;


#4

Until an expert drops in :slight_smile: (e.g. @Paul_Wilkins or @Pullo ) I've cobbled together a working example which is basically proof of concept.

It's working in Edge and Chrome and Firefox just uses the native display:contents. It would work in IE11 if elem.remove() was re-placed with some js that IE11 understands.

It's a bit of a mess at the moment but is just a proof of concept and would need to be tidied up and organised into more intelligent code.

Firefox doesn't use the JS and will use css instead. I did this by adding an element to the end of the page and set it to display:none in the CSS. I then used CSS @supports rrule to check for display:contents support and if so made the element display:block. Therefore if the element was display:block then we know display:contents was supported. I'm sure there's a simpler way to do this but I couldn't find anything specific to display:contents.

I used the matchMedia js to decide whether to wrap or unwrap the sidebar. Of course the original sidebar html needs to be copied in order that the page can be returned to the state it was before. I just placed it next to the element called .child and then removed the other ones. I;m sure there is a much better way to do this.

However, the first task was to get it to work and the above seems to be working so the next step would be to tidy it up and somehow automate the process a little rather than relying on hard coding too many classnames.

Hope it helps anyway :slight_smile:

.


#5

Thanks Paul,
I'll look through it and see if I can wrap my head around it.

I have been cobbling too. I was able to remove the parent with the window.matchMedia but I I'm having trouble getting it replaced when the media query was not true (when I resize the screen above my max-width).

Think I need an else statement for that, but not sure how to do that.

Okay, I see what you mean now.

I was just thinking that since mine was using this if (mql.matches) there would be something similar using else to say "do nothing if it doesn't match" or restate the parent div.

Here is my half working attempt...
I just temporarily disabled my CSS media query for firefox. It removes the parent in IE11 too.

<!doctype html>
<html lang="en">
<head>
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>JavaScript unwrap children</title>
<style>
.wrap {
   width:90%;
   max-width:1000px;
   margin:auto;
   padding:20px;
   outline:1px solid red;
   box-sizing: border-box;
}
.sidebar {
   margin:10px 0;
   padding:40px;
   background:lime;
}
.child {
   background:#eee;
   margin:10px 0;
   padding:10px;
}
@media all and (max-width: 700px) {
   .remove_xx {
      display:contents;
   }
}
</style>
</head>

<body>

<div class="wrap">
   <h2>JavaScript unwrap children</h2>
   <p>Remove green parent div and leave child divs in page at max-width: 700px</p>
   <p>Need a JavaScript substitute for <code>display:contents;</code> in all browsers.</p>
   <div class="sidebar remove">
      <div class="child">child div to remain in page</div>
      <div class="child">child div to remain in page</div>
      <div class="child">child div to remain in page</div>
   </div>
</div>

<script>
var mql = window.matchMedia("screen and (max-width: 700px)")
unwrap(mql) // call listener function explicitly at run time
mql.addListener(unwrap) // attach listener function to listen in on state changes

function unwrap(mql) {
     if (mql.matches){
   // select element to unwrap
	var el = document.querySelector('.sidebar');
	// get the element's parent node
	var parent = el.parentNode;
	// move all children out of the element
	while (el.firstChild) parent.insertBefore(el.firstChild, el);
	// remove the empty element
	parent.removeChild(el);
    } else{
	}
}
</script>

</body>
</html>

#6

You don't want to do nothing because if the window is being resized from smaller there will be no sidebar. You need to copy the original html and then if matchmedia matches you remove the parent and if it doesn't match then in the else condition you put the parent back but first checking that it isn't already there (as in my example).

I have to go out now but back tomorrow.


#7

That's what I would have recommended.

Otherwise, would it be viable to just test the screen width/height and execute based on that?

if($(window).width() >= 1024){
  // do your stuff
}

I know that's jQuery, but it can be converted to vanilla JS if that seems to be a good solution.

Also, you could hook into the resize event, so that the code gets re-evaluated should the user manually resize the screen.


#8

Hi Pullo,
Thanks for pitching in.

I'm just looking for a nice clean polyfill to imitate display:contents;

Paul got the concept right in his codepen from post #4. We're looking for the leanest way to write the script.

This and a toggle menu would be the only scripts I'll be using on the site, so I was really trying to do it all in vanilla js.

EDIT:
If you run this page below in Firefox 56+ you will see it using display:contents; to remove the children from the parent div. It does this when media queries kick in, then it goes back to normal when the media query is not true.

That's what we would like to imitate with vanilla js.

JS removed to show how display:contents work...

<!doctype html>
<html lang="en">
<head>
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>JavaScript unwrap children</title>
<style>
.wrap {
   width:90%;
   max-width:1000px;
   margin:auto;
   padding:20px;
   outline:1px solid red;
   box-sizing: border-box;
}
.sidebar {
   margin:10px 0;
   padding:40px;
   background:lime;
}
.child {
   background:#eee;
   margin:10px 0;
   padding:10px;
}
@media all and (max-width: 700px) {
   .remove {
      display:contents;
   }
}
</style>
</head>

<body>

<div class="wrap">
   <h2>JavaScript unwrap children</h2>
   <p>Remove green parent div and leave child divs in page at max-width: 700px</p>
   <p>Need a JavaScript substitute for <code>display:contents;</code> in all browsers.</p>
   <div class="sidebar remove">
      <div class="child">child div to remain in page</div>
      <div class="child">child div to remain in page</div>
      <div class="child">child div to remain in page</div>
   </div>
</div>

</body>
</html>

#9

Hi,

Sorry I dropped off there — had to do annoying things like sleep and go to work.

Anyway, I'm back now and would be glad to help if I can.

I'm vaguely familiar with display:contents, but only have (at best) intermediate CSS skillz.

To make sure I have understood you correctly, you are looking for someone to look over the code in this CodePen and (if necessary) improve it.

Did I get that right?


#10

Yes that's basically it :slight_smile:

The pen is working perfectly (apart from the ie11 problem with remove() which needs substituting) but we were wondering if the methods used are the most efficient to do what is required.

The matchMedia works perfectly and I wouldn't change that for a script that monitors window resize as that makes pages very slow even when throttled (and then they don't act immediately).

So what's really needed is to maybe have one target class (like 'remove-this') on the parent that you want to remove at a certain width (using matchMedia) then have the script automatically remove that parent and leave the children in place.

Of course the reverse needs to happen when the screen is larger than the max-width media and the parent needs to be replaced around the children. I did this by saving the original html first and then re-instated it when needed and deleting the previous children.

The script in the codepen is doing all these things but I'm sure there is a more efficient way to do this as I was using a class on all the children in order to find them again. Perhaps the js should add the classes as required (or data attributes perhaps) in order that the user just needs to add the remove-this class to the parent.

I was also detecting display:contents support so that Firefox doesn't get the js at all as it does it natively but I'm not sure the method I am using is the correct way to do it although it works fine.

Display:contents makes an element appears as though it is not in the dom at all but leaves all the children in place. Therefore if you have elements that are wrapped in a parent to aid layout you can remove the parent wrap width display:contents so that the children are no longer children of that element. It's useful for mobile because you can linearise everything easily and use flexbox to re-order all items as they are all now siblings.


#11

Hey, I spent a while looking at this and have a couple of questions.

  • Is it important to have this happen for more than one element on a page? If so, what is the expected behavior if there are two separate elements with a class of sidebar, but with differing content?

  • Would it not just be possible to toggle the class of sidebar on and off to achieve the same result?


#12

So, assuming that the answer to both these questions is "no", here's how I would go about doing things.

I would do the feature detection in JavaScript. This makes it more obvious what is happening where:

if (!CSS.supports('display', 'contents')){
  // do stuff
}

Initially sidebar should be in its unwrapped state, so I would next grab the innerHTML of the sidebar parent div. Then I would invoke the unwrap function to remove the sidebar div from the DOM and grab a second reference to the innerHTML of the sidebar parent div. In this second reference, sidebar will be in its unwrapped state.

if (!CSS.supports('display', 'contents')){
  var sidebar = document.querySelector('.sidebar');
  var sidebarParent = sidebar.parentNode;

  var sidebarParentNormal = sidebarParent.innerHTML;
  unwrap(sidebar);
  var sidebarParentAltered = sidebarParent.innerHTML;
}

The unwrap function does exactly what it says on the tin and is fine to use in this case.

Finally, I would attach an event listener to swap out the innerHTML of the sidebar parent div, based on the width of the screen.

Here's a demo. I've included two console.log statements so that you can assure yourself that the script is not running on browsers that support display: content.

<!doctype html>
<html>
  <head>
    <meta charset='utf-8'>
    <title>JavaScript unwrap children</title>
    <style>
      .wrap {
        width:90%;
        max-width:1000px;
        margin:auto;
        padding:20px;
        outline:1px solid red;
        box-sizing: border-box;
      }
      .sidebar {
        margin:10px 0;
        padding:40px;
        background:lime;
      }
      .child {
        background:#eee;
        margin:10px 0;
        padding:10px;
      }

      @media all and (max-width: 760px) {
        .sidebar {
          display:contents;
        }
      }
    </style>
  </head>

  <body>
    <div class="wrap">
      <h2>JavaScript unwrap children</h2>
      <p>Remove green parent div and leave child divs in page.</p>
      <p>A JavaScript substitute for <code>display:contents;</code> in all browsers.<br>How can this script be run with a media query or based on screen widths?</p>
      <p><b>Reduce Browser Window to kick in media query</b><br> Firefox 56+ will use <code>display:contents;</code>.</p>
      <div class="sidebar">
        <div class="child">child div to remain in page</div>
        <div class="child">child div to remain in page</div>
        <div class="child">child div to remain in page</div>
      </div>
      <p>More content..</p>
    </div>

    <script>
      function unwrap(el){
        var docFrag = document.createDocumentFragment();
        while (el.firstChild) {
          var child = el.removeChild(el.firstChild);
          docFrag.appendChild(child);
        }

        el.parentNode.replaceChild(docFrag, el);
      }

      function handleViewportChange(mql, els) {
        if (mql.matches) {
          console.log("Viewport smaller than 761px");
          sidebarParent.innerHTML = sidebarParentAltered;
        } else {
          console.log("Viewport larger than 761px");
          sidebarParent.innerHTML = sidebarParentNormal;
        }
      }

      if (!CSS.supports('display', 'contents')){
        var sidebar = document.querySelector('.sidebar');
        var sidebarParent = sidebar.parentNode;

        var sidebarParentNormal = sidebarParent.innerHTML;
        unwrap(sidebar);
        var sidebarParentAltered = sidebarParent.innerHTML;

        var mql = window.matchMedia("screen and (max-width: 760px)");
        mql.addListener(handleViewportChange);
        handleViewportChange(mql);
      }
    </script>
  </body>
</html>

HTH


#13

For Ray's current page you probably only need to worry about one sidebar but for a more generic solution it would perhaps be better to cater for all elements that have display:contents applied and thus create a polyfill for it. However for the time being it might be prudent to target Rays actual requirement and simplify/streamline the code to that end.

No for flexbox to reorder elements they need to be siblings. For example if you have some items in a left column and some items in a right column you would wrap both in a parent div and place them with whatever layout system you are using.

However, when it comes to a vertical mobile layout it may be best that some of the items in those side columns need to be positioned in a better place in the whole page layout and that can only be done when elements are siblings which means you don't want to move the whole block of children but just one or two children perhaps.

In Rays example he wants one item from his sidebar on top of the content and the other item under the content but as the sidebar is one block it can't be split above and below something else.


#14

Oh ok. Flexbox makes my head hurt. I don't know if you saw it (as we more or less cross-posted), but I just posted a demo above that should do what Ray wants.


#15

That looks great and much neater than my attempt. :slight_smile: I think that's pretty close to what Ray wanted.

(It would eventually be good to handle more than one instance of display:contents but I guess that means copying large chunks of html everywhere.)


#16

Yeah, pretty much. The problem with removing the element from the DOM, is that you're going to need somewhere to hook back into, so as to reinsert it. It'd be much neater/easier to manipulate the element in place in some way that it doesn't affect the display (maybe change it to a span, or an element the browser doesn't recognize) and then toggle it back and forth as necessary. The trouble here is that don't understand enough about the original problem and the use cases. I did do Wes Bos' What the Flexbox course, but that only really scratched the surface. So much to learn ...


#17

Unfortunately that won't work. Whatever element you change it to will still make the children children and not siblings of elements outside the current context.

Here's an example of how display:contents can be used and moves a sidebar both above and below the main content.

Firefox only of course:

<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Untitled Document</title>
<style>
html {
	box-sizing: border-box;
}
*, *:before, *:after {
	box-sizing: inherit;
}
.wrap {
	display:flex;
	max-width:980px;
	margin:auto;
}
.sidebar {
	flex:1 0 20%;
	background:#ccc;
	padding:10px;
}
.main {
	flex:1 0 80%;
	background:red;
	padding:10px;
}


@media screen and (max-width:760px){
	.sidebar{display:contents;}
	.child{background:#f9f9f9;}
	.wrap{flex-wrap:wrap;}
	.wrap > *,.child{flex:1 0 100%}
	.child-below{order:1}
}
</style>
</head>

<body>
<div class="wrap">
  <div class="sidebar">
    <div class="child"><p>On desktops I am in side column - On mobile I am above content</p></div>
    <div class="child child-below"><p>On desktops I am in side column - On mobile I am below content</p></div>
  </div>
  <div class="main">
    <h1>Main Content Goes here</h1>
    <p>More content..</p>
    <p>More content..</p>
    <p>More content..</p>
    <p>More content..</p>
    <p>More content..</p>
    <p>More content..</p>
    <p>More content..</p>
    <p>More content..</p>
    <p>More content..</p>
    <p>More content..</p>
  </div>
</div>
</body>
</html>

You can do this type of thing with css grid but you have to start with the right html and of course you don't have such good browsers support as other methods.


#18

Yes, my initial thoughts were to find the opening div of the element and then immediately add a closing div with js and then set it to display:none so that it always remains in the html. The problem is that you would need to find the closing div also and do much the same In this way the html could be re-assembled from what was already there assuming suitable identifiers were present.

e.g.

Start with this:

<div class="sidebar">
  <div class="child">child div to remain in page</div>
  <div class="child">child div to remain in page</div>
  <div class="child">child div to remain in page</div>
</div>

Then end up with this:

<div class="sidebar" data-visibility="hide" data-tagmatch="01" data-tagposition="open"></div>
<div class="child">child div to remain in page</div>
<div class="child">child div to remain in page</div>
<div class="child">child div to remain in page</div>
<div class="sidebar" data-visibility="hide" data-tagmatch="01" data-tagposition="closed"></div>

However, that was too complicated for me and again I think there's probably an easier way to do it :slight_smile:


#19

Oh nice :slight_smile:

This is what I see on Chrome. Am I missing something?

Understood. Shame though...

Something like that would be my next idea. Ultimately, all you are doing is toggling between two states based on window width, so the easiest thing to do would be to pull those states out when the page loads and toggle between them at will. If you had multiple elements to toggle, you could store them all in an object of some kind and as long as you have a live reference to the parent node, it should all work fine. There shouldn't really be much more code than what we currently have.


#20

I've just noticed that it doesn't run in IE11 while my version was mostly working there?

It seems to be the JS supports rule.

`  if (!CSS.supports('display', 'contents')){`

The technique in my example was working in IE11 but is obviously a bit long winded.