eqcss / eqcss

EQCSS is a CSS Reprocessor that introduces Element Queries, Scoped CSS, a Parent selector, and responsive JavaScript to all browsers IE8 and up

Home Page:https://elementqueries.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Ajax page load - new elements receiving multiple data-eqcss attributes

matthewjumpsoffbuildings opened this issue · comments

in my test SCSS i have this:

@element '.column' {
	eq_this {
		width: eval('distributePercentage($it)');
	}
}
@element 'head' {
	width: eval('matchHeights()')
}

@element '.column .center' {
	eq_this {
		top: eval('verticalCenter($it)');
	}
}

this is working fine on initial page load - but i am using Barba.js to add ajax page loads, and in my Barba 'newPageReady' listener I am calling EQCSS.apply() to ensure the new content that is added to the DOM gets EQCSS styling.

when the EQCSS.apply() is called the new elements are being given multiple data-eqcss attributes, and one set of them isnt updating, and overrides the other with the wrong values

for example if i have this html

<div class="column">
   <span class="center"></span>
</div>

what ends up happening is the <span> becomes <span class="center" data-eqcss-3-1="" data-eqcss-3-0=""></span>

i have an inkling what the issue is, its possibly because the old pages html is still in the DOM when the 'newPageReady' is fired, which calls EQCSS.apply(), but then after the Barba transition the old pages HTML is removed.

If i wait for the new page to animate in, which then removes the old page from the DOM, and then call EQCSS.apply() i dont get the double up of data-eqcss attributes on the html elements.

sorry if this is poorly explained, its hard to set up a codepen that replicates my setup. but im pretty sure the double up of data-eqcss attributes is to do with the removing of the old page HTML from the DOM after calling EQCSS.apply(), im just not sure why or how to fix it

it seems that calling EQCSS.apply() again after the new page animation has finished and the old page is removed, doesnt cleanup the superfluous data-eqcss attributes on the new pages elements

If I build a demo where elements get added to the page, whenever EQCSS.apply() ends up running (manually, or triggered via events) it adds attributes, but I've never observed it adding conflicting attributes to the same page.

Here's a little demo I made to try to reproduce it:

<script>
  setInterval(function(){
    document.body.innerHTML += '<div>test</div>'
  },2000)
</script>

<style>
  @element div {}
</style>

<body>

<script src=http://elementqueries.com/EQCSS.js></script>

I've also recorded using it to demonstrate that in this demo, when new elements are added to the page and EQCSS.apply() runs, sequential numbers get added.

Are you able to record what you're seeing in a similar way so I can at least observe the issue if you're not able to easily isolate and share it?

attributes

ok first reason i dont think your demo is showing the issue is because you are using EQCSS from a CDN, not as a common js module. there does seem to be some difference in performance/behaviour with EQCSS using it with EQCSS.apply() calls set up manually.

here is a screen recording of what im talking about, its 5mb so it might take a while to load:
eqcss issue

as you can see, the doubled up data-eqcss attributes get added after the new page has been ajaxed in and i resize the browser window. also notice in the styles inspector 2 [data-eqcss] rules being applied, one overriding the other. in this example they have the same value, but one of those values stays the same while the other updates when i resize, and usually the one that doesnt update is the one that overrides the one that does

also if you can be bothered, heres a zip of my testing site that im experimenting with EQCSS in.
https://drive.google.com/file/d/0B-0_bp-HwJFlRl9zTm9lRDJKUk0/view?usp=sharing

im using statamic as my CMS, its a flat-file PHP CMS, if you have valet you should just be able to put it in a parked folder and it will work.

the javascript is in /site/themes/rkt/js/bundle.js - its generated via webpack, but its not minified, and if you got to line 108 to 145 you should see the code where Im manually including the EQCSS module, and then running it on doc ready, resize, and barba newPageReady

oh also if you want the source folder for the site let me know.

and if you need a hand getting statamic working also. if you dont have valet i guess whatever local dev php environment you have, it should work pretty easy, theres no config needed you can just drag all the files into a top level local host domain and it should work

Hey @matthewjumpsoffbuildings, thanks for recording the video, and thanks for sharing the project file with me. I wasn't able to get the code running, or find the source, but I did see EQCSS in the compiled bundle in the theme folder.

After looking at the video, and the details that I can see here, to me it looks like you would normally be importing EQCSS into a module, which adds event listeners to the window, document, and sometimes other elements in the document as well depending on the conditions you use. As well, the plugin loads and maintains a copy of all @element queries it has read, adds unique attributes to elements, and then babysits the application of these styles.

Looking at what's going on here it's as though after the AJAX page load, the first imported plugin it still running and babysitting its own styles, but a second instance of the plugin seems to be running, trying to babysit the elements, and interfering with the event listeners from the first instance.

I don't have a lot of experience with JS modules and these sorts of workflows, is it possible to ensure that you're loading it only once, no matter how many AJAX page loads are happening?

Does any of that sound familiar, like something that could be happening?

The closest I've come to reproducing this is the following two examples. In the first, I've loaded EQCSS twice, and it has attached itself to window both times. When the second one loads, because they have the same name, even if it's overwriting the plugin with a verbatim copy of itself, there's no conflict.

<div>EQCSS && EQCSS</div>

<style>
  @element div {
    $this {
      background: lime;
    }
  }
</style>

<script src=http://elementqueries.com/EQCSS.js></script>
<script src=http://elementqueries.com/EQCSS.js></script>

However, I do see something similar to what you're describing if I duplicate EQCSS and rename it /eqcss/gqcss/ and then load two copies of the same plugin alongside each other:

<div>EQCSS && EQCSS clone</div>

<style>
  @element div {
    $this {
      background: lime;
    }
  }
</style>

<script src=http://staticresource.com/gqcss.js></script>
<script src=http://elementqueries.com/EQCSS.js></script>

I'm not sure if I'll be able to solve this one, but I'm hoping this helps point you in the right direction for finding a solution. If something like this is happening, is it possible another solution might be to load it via its own <script> tag? It doesn't need to be CDN hosted, but perhaps treating it like a shim or polyfill instead of a JS module could prevent this from happening.

i just switched to including EQCSS via a script tag in and im still seeing duplicate [data] attributes on ajax page load.

the thing you dont seem to be testing is manually calling EQCSS.apply() after removing elements that were under EQCSS control AND adding new elements that need to be controlled by EQCSS AND THEN resizing the browser window.

this is the order of actions that appears to cause the issue

  • add some elements that have element queries to the DOM and call EQCSS.apply()
  • remove them
  • add some new elements that match the same element queries as the old elements
  • re-run EQCSS.apply()
  • resize the browser window
  • look at the [data] attributes of the new elements, to see if they have duplicates

im going to see if i can make a codepen that shows this issue, ill let you know how i go

ok heres a codepen showing the bug

https://codepen.io/matthew-prasinov/pen/JWeORQ

im using a JS template to emulate an Ajax page load, plus the matchHeights function you wrote and some verticalCentering. pardon the ugly jquery/JS :)

click the button to simulate an AJAX page load, insertion of the new page, and removal of the old page. then right click > inspect the green box and notice it now has 2 conflicting [data-eqcss] attributes, and its no longer properly vertically centered

ps im using chrome on OSX Sierra, could you check in that?

edit: just checked in Safari, seems its doing the same thing

So I've re-worked the example and I think I have it working now:

  • removed jQuery for clarity
  • changed <script type=text/template> to <template> for clarity
  • removed column-height function
  • removed centering function
  • moved EQCSS.apply() until after the first page was removed

If we know you have a set of elements side-by-side in the same parent we can use EQCSS to match those heights. The match-height JS would be capable of matching any elements anywhere in the DOM, but in the use case you've given me I was able to match them with:

@element .columns {
  $this .column {
    width: calc(100% / eval('children.length'));
    height: eval("
      var seen = [];
      var tag = document.querySelectorAll('.column');
      for (var i=0; i<tag.length; i++) {
        tag[i].style.height = 'inherit';
        seen.push(tag[i].offsetHeight);
        tag[i].style.height = '';
      }
      Math.max.apply(Math, seen);
    ")px;
  }
}

If the goal of the center element is to stay in the center, that's something we can do with CSS like this:

.column .center {
  position: absolute;
  left: 0;
  top: 50%;
  transform: translateY(-50%);
}

Does this example do what you need to do?

<div id=wrap>

  <button id=do>Remove/Add</button>

  <div class=columns>
    <div class=column>
      <p> as kdsjalksjd lkasj laksjdlakjs lkajsldk alskdj alkjsdl kajsl kajsldkajslkd jalksjd lakjsd</p>
    </div>
    <div class=column>
      <p>asd kajskjda kjdhsajkhs kjaskjd</p>
      <span class=center></span>
    </div>
    <div class=column>
      <p>ask daskj kajsd jkadsjkha ksdhjkah jkaskjd akjnsd a dkanskdj naskj akjfbkwjbq kjwdbqjkba ksbmnab sdmnabmns bdansbd kasbdk abskdb akjsbd kajsbdk abda</p>
    </div>
  </div>

  <!-- This is a fake ajax item -->
  <template>
    <div class="columns ajax">
      <div class=column>
        <p>das kdsjalksjd lkasj laksjdlakjs lkajsldk alskdj alkjsdl kajsl kajsldkajslkd jalksjd lakjsd</p>
      </div>
      <div class=column>
        <p>asd kajskjda kjdhsajkhs kjaskjd</p>
        <span class=center></span>
      </div>
      <div class=column>
        <p>ask daskj kajsd jkadsjkha ksdhjkah jkaskjd akjnsd a dkanskdj naskj akjfbkwjbq kjwdbqjkba ksbmnab sdmnabmns bdansbd kasbdk abskdb akjsbd kajsbdk abda</p>
      </div>
    </div>
  </template>

</div>


<style>
  .column {
    float: left;
    background: grey;
    position: relative;
  }
  .column .center {
    display: block;
    background: green;
    height: 30%;
    width: 100%;
    position: absolute;
    left: 0;
    top: 50%;
    transform: translateY(-50%);
  }

  @element .columns {
    $this .column {
      width: calc(100% / eval('children.length'));
      height: eval("
        var seen = [];
        var tag = document.querySelectorAll('.column');
        for (var i=0; i<tag.length; i++) {
          tag[i].style.height = 'inherit';
          seen.push(tag[i].offsetHeight);
          tag[i].style.height = '';
        }
        Math.max.apply(Math, seen);
      ")px;
    }
  }

</style>


<script>
  var template = document.querySelector('template')

  var wrap = document.querySelector('#wrap')

  document.querySelector('#do').addEventListener('click', function() {

    var oldPage = document.querySelector('.columns')

    wrap.insertAdjacentHTML('afterend', template.innerHTML)

    // normally with ajax page transitions the oldPage remove would happen 1 second or more later
    // after the full new page transition animation is complete
    oldPage.remove()

    // has to be applied ASAP? the new page is about to be animated in so it needs to look right
    EQCSS.apply()

  })
</script>

<script src=http://elementqueries.com/EQCSS.js></script>

i do appreciate you taking the time to rework my example, but this is not about the specific code in my example. the codepen i posted was me testing EQCSS trying to work out how we can use it in all our future projects and looking for bugs. So while its great that youve given me a better version of the code, im not asking for that - im hoping you can look at that pen I posted, and work out why the [data-eqcss] attributes are getting applied multiple times and overriding each other.

I inserted your code into a pen, and simply changed one thing - the order of EQCSS.apply() and it breaks again.

https://codepen.io/matthew-prasinov/pen/YZdPbL

as i explained in the comment just above the EQCSS.apply() in the code you copy pasted:

// has to be applied ASAP? the new page is about to be animated in so it needs to look right

in almost all ajax/pjax sites - the old page is not removed until the new page has fully faded/slid/animated in. and the new page, while animating in needs to look right so EQCSS.apply() has to be called after the new page is added, but BEFORE the old page is removed

notice that now, if the EQCSS.apply() is before the oldPage.remove() (which it has to be), if you click the Remove/Add button, and then resize the window, that the grey .columns now do not correctly change heights anymore as the text grows or shrinks. And if you right click inspect the elements you will see .column has multiple [data-eqcss] attributes and one is overriding the other.

And if i call EQCSS.apply() 2 times, once on adding the new page, and second AFTER removing the old page, its still broken. The second EQCSS.apply() does not clean up the mess.

please look at this codepen: https://codepen.io/matthew-prasinov/pen/YZdPbL
and dont worry about reworking the code, its not actually important at all.

what is important is, why does calling EQCSS.apply() BEFORE removing the old page result in the new page being ruined with conflicting [data-eqcss] attributes.

one solution to this bug might be allowing EQCSS.apply() to be passed a context, a class or id or html element that it only looks at elements inside it, that way you could do

EQCSS.apply(newPage)

and hopefully the stuff in old page wouldnt interfere? im not sure how trivial that is.

another point - this goes back to a comment i asked in the first issue i posted when i started experimenting with EQCSS - #55 (comment)

and that is - i really think EQCSS needs some kind of EQCSS.cleanup() function. some kind of garbage collection that removes any unneeded styles from head, and unneeded [data-eqcss] attributes from page elements.

Hey @matthewjumpsoffbuildings, I was thinking something similar - I had the idea what if you could go through the document and remove all of the exiting data-eqcss- attributes, then reload the styles. Here's what I came up with for a EQCSS.cleanup() function:

<div id=wrap>

  <button id=do>Remove/Add</button>

  <div class=columns>
    <div class=column>
      <p> as kdsjalksjd lkasj laksjdlakjs lkajsldk alskdj alkjsdl kajsl kajsldkajslkd jalksjd lakjsd</p>
    </div>
    <div class=column>
      <p>asd kajskjda kjdhsajkhs kjaskjd</p>
      <span class=center></span>
    </div>
    <div class=column>
      <p>ask daskj kajsd jkadsjkha ksdhjkah jkaskjd akjnsd a dkanskdj naskj akjfbkwjbq kjwdbqjkba ksbmnab sdmnabmns bdansbd kasbdk abskdb akjsbd kajsbdk abda</p>
    </div>
  </div>

  <!-- This is a fake ajax item -->
  <template>
    <div class="columns ajax">
      <div class=column>
        <p>das kdsjalksjd lkasj laksjdlakjs lkajsldk alskdj alkjsdl kajsl kajsldkajslkd jalksjd lakjsd</p>
      </div>
      <div class=column>
        <p>asd kajskjda kjdhsajkhs kjaskjd</p>
        <span class=center></span>
      </div>
      <div class=column>
        <p>ask daskj kajsd jkadsjkha ksdhjkah jkaskjd akjnsd a dkanskdj naskj akjfbkwjbq kjwdbqjkba ksbmnab sdmnabmns bdansbd kasbdk abskdb akjsbd kajsbdk abda</p>
      </div>
    </div>
  </template>

</div>


<style>
  .column {
    float: left;
    background: grey;
    position: relative;
  }
  .column .center {
    display: block;
    background: green;
    height: 30%;
    width: 100%;
    position: absolute;
    left: 0;
    top: 50%;
    transform: translateY(-50%);
  }

  @element .columns {
    $this .column {
      width: calc(100% / eval('children.length'));
      height: eval("
        var seen = [];
        var tag = document.querySelectorAll('.column');
        for (var i=0; i<tag.length; i++) {
          tag[i].style.height = 'inherit';
          seen.push(tag[i].offsetHeight);
          tag[i].style.height = '';
        }
        Math.max.apply(Math, seen);
      ")px;
    }
  }

</style>


<script>
  var template = document.querySelector('template')

  var wrap = document.querySelector('#wrap')

  document.querySelector('#do').addEventListener('click', function() {

    var oldPage = document.querySelector('.columns')

    oldPage.insertAdjacentHTML('afterend', template.innerHTML)

    oldPage.remove()

    EQCSS.cleanup()

  })
</script>

<script src=http://elementqueries.com/EQCSS.js></script>

<script>
  EQCSS.cleanup = function () {

    EQCSS.data = []

    var el = document.querySelectorAll('html')

    for (var i=0; i<el.length; i++) {

      var tag = el[i].parentNode.parentNode ? el[i].parentNode.parentNode.querySelectorAll('*') : el[i].querySelectorAll('*')
      var found = []

      for (var j=0; j<tag.length; j++) {

        for (var k = 0; k < tag[j].attributes.length; k++) {

          if (tag[j].attributes[k].name.indexOf('data-eqcss-') == 0) {

            console.log(tag[j].attributes[k].name)
            tag[j].removeAttribute(tag[j].attributes[k].name)

          }

        }

      }

    }

    EQCSS.load()

  }
</script>

So the key features here are:

  • you call it after the old HTML has been removed
  • it resets the loaded styles from EQCSS.data
  • it removes all data-eqcss-* attributes from elements in the DOM
  • finally reloads the styles with EQCSS.load()

After running this, you should have a clean slate for your AJAX-loaded page :D

You can run this from your code, or if you have a way to use Mutation Observers (and you have the browser support you need) that might be a way you could 'watch' elements for changes instead of having to manually run it yourself from JS!

sweet, in some preliminary tests that seems to fix the issue. do you think its possible to have an EQCSS.cleanup() function like this added into EQCSS permanently?

also does it really need to loop through all html elements? cant it use the EQCSS.data[] array that already exists, and get the selector property that is already being stored in there, and just select those items to clean up? might save some CPU?

I'm definitely thinking some kind of an EQCSS.cleanup() or EQCSS.reset() function could be included in a future release, similar to this, but with more flexibility like you mention. I had thought maybe we could pass in our own selectors (for the DOM) and remove attributes from that selector and its parents down, but I ran into some problems with that. More experimentation required. Perhaps we could add an EQCSS.reset() function sooner and then release an EQCSS.cleanup() function after we've figured out how this would work better!

I'm going to close this issue for now - but I'm working on the EQCSS.reset() function and will ping you when it's released! Thanks so much for reporting this issue and working through this with me :D

Hey @matthewjumpsoffbuildings! I just pushed EQCSS v.1.6.0 which adds an EQCSS.reset() function for your use case and many others :D