jquery-pjax icon indicating copy to clipboard operation
jquery-pjax copied to clipboard

Calling pjax only after CSS transition out has ended?

Open nearestnabors opened this issue 10 years ago • 23 comments

I've been trying to figure out how to do this. Most of the replies I see here use jQuery's animation functions, which I do not use.

I use CSS transitions to move between a loading and a loaded state. Among other properties, the opacity goes from 1 to 0 when a link has been clicked, and 0 to 1 when the content has been loaded by virtue of CSS associated with data attributes indicated loading/loaded on the body respectively.

Problem I ran into: when a person clicks a link, pjax isn't able to wait for the loading transition to finish. I cannot even add a timeout. Reading through the issues, this is the best I was able to come up with, but it's so buggy, I've had to turn it off. (Buggy meaning sometimes you have to click a link twice for it to work and the back button refreshes to the previous page.)

Please tell me there is a better way to do this that is more in line with how this library does things.

$body.on('click', "a[href^='"+siteURL+"'], a[href^='/'], a[href^='./'], a[href^='../'], a[href^='#'], a:not(a[target='_blank'])", function(event) {
        var clickery = event;
        // .loading transitions opacity to 0
        $body.attr('data-page-status', 'loading');

        if (window.transitionEnd) {
            // trigger pjax post-transition only for one transitionend
            if (e.originalEvent.propertyName === "opacity") {
                $("#content").on(window.transitionEnd, function(e){
                    $.pjax({
                        url: clickery.target.href,
                        container: '#content',
                        fragment: '#content'
                    });
                });
            }
        } else {
            // a crappy timeout if no transitionend
            setTimeout( function(){
                $.pjax.click(clickery,{
                    url: clickery.target.href,
                    container: '#content',
                    fragment: '#content'
                });
            }, customPjaxTimeout);
        }

    // STOP THE LINK FROM WORKING NORMALLY
    return false;
});

nearestnabors avatar Mar 08 '15 21:03 nearestnabors

Giving this a gentle nudge.

I'd love to feature PJAX, specifically this PJAX plugin, in some of my animation educational materials. But I feel like transitioning-out is something that has to get nailed down first. Otherwise, it's still a jump cut, even if it's followed by a transition-in.

If I understood this problem more, maybe I could even help :)

nearestnabors avatar Mar 12 '15 00:03 nearestnabors

It seems like you've done your due dilligence before posting this, but for other readers: https://github.com/defunkt/jquery-pjax/issues/17#issuecomment-5207025 #159 #168 #350

There are several things that stand out to me in your code. First of all, the example seems incomplete. You're using the e variable before it's defined. I can't tell what window.transitionEnd is. I can't see how data-page-status attribute kicks off CSS transitions. I don't understand why you still want a timeout if the browser doesn't support CSS transitions. Shouldn't the browser just go ahead and perform pjax immediately if CSS transitions are unavailable?

Lastly, you're using $.pjax.click() to react to an event (after short timeout) that you already defaultPrevented by return false from jQuery handle. $.pjax.click() is designed to abort any pjax processing if your clickery event is already cancelled, which you make sure it is.

I propose something along the following, which I haven't tested:

# Handle clicks on all links
$('a').pjax
  container: '#content'
  fragment: '#content'

# Detect the name of the "transitionend" event:
transitionend = do ->
  testEl = document.createElement 'div'
  return 'webkitTransitionEnd' if 'WebkitTransitionProperty' of testEl.style
  return 'transitionend' if 'MozTransitionProperty' of testEl.style

preventNextPjax = false

# "pjax:click" will fire only for hyperlinks to pages on the same site.
# Clicks to external links as well as to `href^="#"` will be handled natively.
$(document).on 'pjax:click', (event, options) ->
  link = event.target
  return false if link.target is '_blank'  # allow native load in new window

  if transitionend
    # Do whatever you need to fire off the CSS transition:
    $(document.body).addClass 'loading'

    # Bind the "transitionend" handler once to fire off the real pjax request:
    options.container.one transitionend, -> $.pjax options

    # Hack to prevent the pjax request that would fire before the transition has ended
    preventNextPjax = true

$(document).on 'pjax:beforeSend', ->
  if preventNextPjax
    preventNextPjax = false
    return false

The huge problem with both of our solutions is that the transition time delays the actual making of the server request. So if the server request only takes 100ms to complete, because of the 200ms transition time the user will have to wait at least 300ms in total to be presented with the new content. This is why I strongly advise against combining page transitions with pjax at this moment.

Ideally, we would fire off the server request in the background at the same time that we start the transition. Then we would delay inserting the new HTML content until the transition has completed. This is not possible with the current version of pjax, but is planned for the rewrite.

mislav avatar Mar 12 '15 05:03 mislav

Hello,

Here is what i did on my website http://proov.fr, i'm a javascript beginner but i watched a lot of tutorials ^^

i'm using Wordpress, pjax.js and velocity.js and css3 animations. I think CSS3 animations are better for this kind of effects because they can be triggered at the initial full page load, even if there is no pjax request.

on all the pages templates I include this on top (under header):

<script>
    var pjaxy_page_info = {
        body_class: "<?php echo esc_js( join(' ', get_body_class())); ?>",
        page_title: "<?php wp_title( '|', true, 'right' ); ?>",
        current_section: "<?php echo $current_section; ?>",
        current_slug: "<?php echo $current_slug; ?>",
    }
</script>

before </body> and others JS, i include this

<script>
    var animated_el = document.querySelectorAll(".animate");
</script>

i select all the .animate divs in the page, i wait until they're finished and then i launch the pjax request.

in my main script.js i have this:

(function($){
    /* Vars */
    var bod = $("body");
    $pjax_container = $('#main');
    $overlay_container = $('#overlay');
    $items_overlays = $('.overlay-color');

    /* Self invoking Functions */
    var owl_home = (function owlCarousel(){
            // Do stuffs
        return owlCarousel;
    })();
    var masonry_init = (function initMasonry(){
        // Do stuffs
        return initMasonry;
    })();

    /* Pjax Begin / Animations Out */
    if ($.support.pjax) {

            $(document).on('click', '.menu a, .logo a, .footer-links a, a.pjax-link', function(event) {
                event.preventDefault();
                event.stopPropagation();

                // Animations Leave
                bod.removeClass("enter").addClass("leave");

                // Vars
                var url = $(this).attr("href");
                var count_anim = 0;

            // Correct animation event
            var animEndEventNames = {
                'WebkitAnimation' : 'webkitAnimationEnd',   // Saf 6, Android Browser
                'MozTAnimation'   : 'animationend',             // only for FF < 15
                'animation'       : 'animationend'                  // IE10, Opera, Chrome, FF 15+, Saf 7+
            },
            animEndEventName = animEndEventNames[ Modernizr.prefixed('animation') ];

            // Animation End
                $(animated_el).one(animEndEventName, function(){
                    count_anim++;
                    if(count_anim === animated_el.length){
                        // Everything is done
                        $.pjax({
                        url: url,
                        container: "#main",
                        fragment: "#main",
                        timeout: 20000,
                    });
                    }
                });
        });

        /* Pjax End / Animations In */
        $(document).on("pjax:end", function(){

            // Change page title
            $('head title').html(pjaxy_page_info.page_title);

            // Re-Init scripts after page loaded
            if(pjaxy_page_info.current_section === "accueil"){
                owl_home();
                    // Others scripts
            }
            if(pjaxy_page_info.current_section === "works"){
                masonry_init();
            }
            // Body default classes
            bod.attr('class', pjaxy_page_info.body_class);

            // Animations Classes In 
            bod.removeClass("leave").addClass("enter");

        });

    }

})(jQuery);

In my HTML i add .animate class on what i want to animate and specific .animate--fade or .animate--slide or .animate--whatever. Eg:

<section class="main-content animate animate--fade">...</section>

and CSS:

// Generic - Default values
.animate {
    -webkit-animation-duration: .25s;
            animation-duration: .25s;
    -webkit-animation-timing-function: ease-in;
            animation-timing-function: ease-in;
    -webkit-animation-fill-mode: both;
            animation-fill-mode: both;

    .leave &{
        -webkit-animation-timing-function: ease-out;
                animation-timing-function: ease-out;
    }
}
// Fade
.animate--fade {
    -webkit-animation-name: fadeIn;
            animation-name: fadeIn;

    .leave &{
        -webkit-animation-name: fadeOut;
                animation-name: fadeOut;
    }
}

// Specific page animations (Delay / Stagger / Easing)
.animate{

    // Works
     .works &#main,
    .works.enter &#main{
        -webkit-animation-delay: 0; // Disable fade on enter
                animation-delay: 0; // Disable fade on enter
    }
    .works.leave &#main{
        -webkit-animation-delay: 99s; // Disable fade on leave
                animation-delay: 99s; // Disable fade on leave
    }

}

It might be not so elegant because i'm a beginner but everything works as expected :)

Sry for my english, i'm french :D

edit: edited some JS (pjax timeout)

proov avatar Apr 02 '15 12:04 proov

@proov Thanks for sharing! :+1:

mislav avatar Apr 02 '15 13:04 mislav

@proov love your site! Really great animations but I've noticed that it's not working as expected, it appears that the page attempts to change to the url destination immediately as the link is clicked but then stops and then goes again after the animation has finished, so it's very jumpy. It's as if the preventDefault is being ignored.

I took a quick video to illustrate, I tested this on recent (potentially latest) versions of Chrome and Firefox on OSX (Yosemite).

Any ideas what the issue could be?

mikeebee avatar Apr 22 '15 02:04 mikeebee

@mislav Getting back to this.

You're using the e variable before it's defined.

Yeah, what's up with that?

I can't tell what window.transitionEnd is.

It's set by a script that checks what prefix transitionEnd uses in that browser. That prefixed version is stored in window.transitionEnd

I can't see how data-page-status attribute kicks off CSS transitions.

It's a scoped class. As in body[data-page-status='loading'] .animatedThing { transform: translateX(100%); } It's like a poor man's single state machine.

I don't understand why you still want a timeout if the browser doesn't support CSS transitions. Shouldn't the browser just go ahead and perform pjax immediately if CSS transitions are unavailable?

What's up with that, too? removes

The huge problem with both of our solutions is that the transition time delays the actual making of the server request. So if the server request only takes 100ms to complete, because of the 200ms transition time the user will have to wait at least 300ms in total to be presented with the new content. This is why I strongly advise against combining page transitions with pjax at this moment.

300ms is not a horribly long wait. Until the combined effect takes greater than 1sec, the perceived speed + UX gain is worth it in most cases. So in this case, I think it's worth it.

Ideally, we would fire off the server request in the background at the same time that we start the transition. Then we would delay inserting the new HTML content until the transition has completed. This is not possible with the current version of pjax, but is planned for the rewrite.

I'd still prefer this. It's an ideal. When can we hope for it, and is there anything I can do to assist?

nearestnabors avatar Apr 22 '15 05:04 nearestnabors

Hello @mikeebee ! yeah i noticed this issue too :(

i really dont know why... maybe if someone could help me to find what i did wrong ^^

Thanks for the feed back ;)

proov avatar Apr 22 '15 09:04 proov

@proov

I modded your code a bit and had a try on my own site.

update Looks like it works like a charm!

if ($.support.pjax) {
    $(document).on("click", "a[href^='"+siteURL+"'], a[href^='/'], a[href^='./'], a[href^='../'], a[href^='#'], a:not(a[target='_blank'])", function(e){

        e.preventDefault();

        $body.attr('data-page-status', 'loading');
        // Vars
        var url = $(this).attr("href");

        $("#content").one(window.transitionEnd, function(){
            $.pjax({
                url: url,
                container: "#content",
                fragment: "#content"
            });
        });
    });

    $(document).on('pjax:end', function(event) {
        $body.attr('data-page-status', 'loaded');
    });
}

nearestnabors avatar Apr 22 '15 17:04 nearestnabors

When can we hope for it, and is there anything I can do to assist?

I'm working on a Promise-based pjax implementation that will make this level of configuration possible. It will take some time, so don't hold your breath.

But let me explain why the design of the library doesn't support this yet. The main goal of pjax is to speed-up user-perceived page loads. It does so by having the server render less (we configure GitHub to not render page layout on pjax requests), and by injecting just the partial page content into the current page, skipping the overhead of executing page styles/javascripts again (as it would after normal refresh). The saving we get on GitHub.com is quite substantial. For the purpose of this conversation, let's say that we speed up page loads by ~200ms. If you add a page transition that is configured to take 200ms to present the user with new content, then you're undoing the potential speed saving that pjax is getting you, and by that practice essentially going against what pjax library design is all about.

I'm sorry for being an apologist for our library's unyielding configuration, but I'm just putting into perspective the use case from which the library was born. Next up will be the version of the pjax that will be a drastic departure of what it is right now, and will allow you to use it and abuse it in any way possible.

mislav avatar Apr 23 '15 09:04 mislav

@mikeebee Hey Mike, i finally found the issue! it was the default timeout who forced a page full refresh. I temporary increased the 650ms default timeout to 20000 ^^, can you tell me if you're still experiencing the bug ?

@mislav I understand :) Pjax.js is wonderfull, I always wanted to build a website with ajax/pushstate to improve the user experience by providing subtle animations. As a webdesigner, i dont have the knowledge to do it, but Pjax.js is well documented and easy to understand. I tried smoothState.js too, but it is not as flexible as Pjax.js.

@rachelnabors Hey rachel, i'm glear to read this :) Maybe you will have to increase the default timeout to avoid a full page refresh ;)

proov avatar Apr 23 '15 16:04 proov

I'm not experiencing a page refresh at rachelnabors.com... Yet. How did you increase the default timeout? On Thu, Apr 23, 2015 at 9:21 AM PREVOST Sylvain [email protected] wrote:

@mikeebee https://github.com/mikeebee Hey Mike, i finally found the issue! it was the default timeout who forced a page full refresh. I temporary increased the 650ms default timeout to 20000 ^^, can you tell me if you're still experiencing the bug ?

@mislav https://github.com/mislav I understand :) Pjax.js is wonderfull, I always wanted to build a website with ajax/pushstate to improve the user experience by providing subtle animations. As a webdesigner, i dont have the knowledge to do it, but Pjax.js is well documented and easy to understand. I tried smoothState.js too, but it is not as flexible as Pjax.js.

@rachelnabors https://github.com/rachelnabors Hey rachel, i'm glear to read this :) Maybe you will have to increase the default timeout to avoid a full page refresh ;)

— Reply to this email directly or view it on GitHub https://github.com/defunkt/jquery-pjax/issues/498#issuecomment-95639154.

nearestnabors avatar Apr 23 '15 16:04 nearestnabors

@proov that's looking great now

@mislav totally understandable and thanks for clarifying, I know people must be requesting this often.

mikeebee avatar Apr 24 '15 00:04 mikeebee

@rachelnabors see https://github.com/defunkt/jquery-pjax#pjax-options

staabm avatar Apr 24 '15 07:04 staabm

@mislav @rachelnabors

I'm using something pretty similar, but I have a problem that it successfully calls pjax, I can see it in the console, but then it normally loads the page rather than sticking with pjax. My setup for the rest of the site works no problem, it's only this new code that seems to break it. Any thoughts?

Updated to include full code; could it be using the same container? I tested the timeout using:

$(document).on('pjax:timeout', function(event) {
    console.log('test');
});

And it didn't turn the console.log so the timeout isn't being reached...

//
$.pjax.defaults.scrollTo = false;
$.pjax.defaults.timeout = 200000;

if ($.support.pjax) {
    $(document).on('click', 'a[data-pjax-detail]', function(e) {
        e.preventDefault();
        $('.loader').show();
        $('body').removeClass('detail-loaded');
        if ( $(this).is('.prev') ) {
            $('body').addClass('detail-loading-prev');
        } else {
            $('body').addClass('detail-loading-next');   
        }
        var url = $(this).attr('href');
        $('#pjax.content').one(window.transitionEnd, function() {
            $.pjax({
                url: url,
                container: '#pjax.content',
                fragment: '#pjax.content',
                timeout: 200000
            });
        });
    });
}

$(document).pjax('a[data-pjax]', '#pjax.content');

$(document).on('ready pjax:start', function() {
    $('.loader').show();
});
$(document).on('ready pjax:end', function() {
    var bodyClass = $('meta[name=body-class]').attr('content');
    if (bodyClass !== undefined) { document.body.className = bodyClass; }
    ajaxInit();
    $('.works-container .each-work .inner .text-container').hide();
    $('.header-container.mobile .mobile-menu-container .menu-trigger').removeClass('active').addClass('non-active');
    $('.header-container.mobile .mobile-menu-container .menu').hide();
    imageSizes();
    $('body').removeClass('detail-loading-prev detail-loading-next').addClass('detail-loaded');
    $('.loader').fadeOut();
});

I preserved the console log in Chrome and it states this:

screen shot 2015-07-22 at 21 41 46

richgcook avatar Jul 22 '15 20:07 richgcook

My first guess would be to check the HTML response from the initial XHR request and check whether the markup in that HTML includes the #pjax.content element.

mislav avatar Jul 22 '15 22:07 mislav

I'll check that but I know it exists in the HTML as I wrote it... but is there any reason why it wouldn't? If it worked first, then navigates away, surely the container exists?

Thanks for the help.

richgcook avatar Jul 22 '15 22:07 richgcook

You can see here, it gets the pjax request, loads the image, then, navigates to the page and reloads it etc.

screen shot 2015-07-23 at 00 06 14

richgcook avatar Jul 22 '15 23:07 richgcook

I removed fragment: '#pjax.content', and it seems to be working now? Any thoughts on this?

richgcook avatar Jul 22 '15 23:07 richgcook

I'll check that but I know it exists in the HTML as I wrote it... but is there any reason why it wouldn't?

I don't know because I didn't write your server-side code. Only you can check whether the HTML returned from the XHR call includes such an element:

<body>
  <div id=pjax class=content>
     ...
  </div>
</body>

mislav avatar Jul 22 '15 23:07 mislav

Fair point; it's definitely there and I updated my problem with the following:

I removed fragment: '#pjax.content', and it seems to be working now? Any thoughts on this?

Any reason why this would solve it?

richgcook avatar Jul 23 '15 07:07 richgcook

When you specify the fragment: option, that instructs pjax to look for the element matching that selector in the response HTML and extract just its contents, not the entire response. If it can't find the target element, it aborts and reloads the page.

I can't tell why your site reloaded the page even when that element was definitely in the response. But, when you removed the option, you stopped telling pjax to look for a specific element in the response, and pjax won't force a reload anymore (unless the response is empty).

mislav avatar Jul 24 '15 22:07 mislav

i found another solution, not 100% satisfactory but its a start

.hide-pjax {
    opacity: 0;
    -webkit-transform: translate(50%,0);
            transform: translate(50%,0);
    -webkit-transition: all 0.5s ease-in-out;
            transition: all 0.5s ease-in-out;
}
// call pjax
$(document).pjax('a', '.pjax-container');
// add the hide class before going to server
$(document).on('pjax:start', function() {
    $('.pjax-container').addClass('hide-pjax');
});
// wait for animation to finish and remove the class
$('.pjax-container').on('transitionend webkitTransitionEnd oTransitionEnd', function() {
    $('.pjax-container').removeClass('hide-pjax');
});

ctf0 avatar Jun 13 '16 10:06 ctf0

I may have found a simple way to delay the click. Just time your animations accordingly.

$(document).ready(function() {
	var i = 0; //the switch
	$("a").click(function(event) {
		console.log(i); //just checking the switch
		var clickit = $(this); //make it available inside other functions
		if (i == 0) {
			event.preventDefault(); //first click, so do not follow click
			i = 1; //set the switch to 'on'
			setTimeout(function() {
				clickit[0].click();
				i = 0;
			}, 1500); // wait 1500ms to click again and turn the switch off again
		}
	});
	$(document).pjax('a', '#pjax-container', { // the usual stuff
		fragment: "#pjax-container"
	});
});

josblomsma avatar Oct 18 '17 16:10 josblomsma