Kirby ajaxed (quick and dirty)


#1

Hi there

I was thinking up of ways to get some fancy ajax page loads in kirby after seeing nodebb and after trying some solutions with kirby’s ajax, found out pjax.
From github:

pjax works by grabbing html from your server via ajax and replacing the content of a container on your page with the ajax’d html.

That’s what i meant by “quick and dirty”. This is not a real json request or anything, we’re still requesting the html. But in style.

What we need

Request pjax.js and nprogress.js for loading and status indication.
Add nprogress’s css.
Define in your html body what is your content container.
Initialize the jQuery and do some css fixes.

Step 1

Put the .js on your header
Github link for pjax.js
Github link for nprogress.js

<?php echo js('assets/js/jquery.pjax.js') ?>
<?php echo js('assets/js/nprogress.js') ?>

I’m assuming you are already requesting jQuery on your template. If not, let’s do that too.

<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>

Step 2

Add nprogress’s css to your css.
Github link to nprogress.css

Step 3

Add id="pjax-container" to your main content.

Step 4

Now comes the dirty.
I’ve tried documenting what is going on directly on the code.

<script type="text/javascript">
    $(function () {
        function menuActive() { // Function to toggle active menu links
            $('.active').removeClass('active'); // Remove "active" class on menu links after an ajax call
            var pgurl = window.location.href; // Get the page url
    // var pguri = window.location.href.substr(window.location.href.lastIndexOf("/") + 1);
    // "uri" gets only the link folder while "url" gets the entire link
    // Left here in case anyone out there uses $p->uri() on the menu
            $(".menu a").each(function () {
                if ($(this).attr("href") == pgurl || $(this).attr("href") == '') { // Compare url to links
                    $(this).addClass("active"); // Set "active" class on menu links
                    $(this).parents('li').children('a').addClass('active'); // Set "active" class on the parent of submenu links
                }
            });
        }
    // Requires jQuery 1.7+ to utilize the new "on" event attachment
        $("body").on("click", "a", function (event) {
            $.pjax({
                "url": $(this).attr("href"),
                "fragment": "#pjax-container",
                "container": "#pjax-container",
                "complete": function (data) { // Want to do something else? Left here for reasons
                }
            });
            event.preventDefault();
        });
        $(document).on('pjax:start', function () {
            NProgress.start(); // Start the nprogress bar
        });
        $(document).on('pjax:end', function () {
            NProgress.done(); // End the nprogress bar
            menuActive(); // Update the "active" class on links after ajax call
        });
        $(document).ready(menuActive); // Update the "active" class on links on first load
    });
</script>

Aaaaand we’re done.
Check out a vanilla kirby installation example.

Also, it was done this way because I thought the normal method was bit complicated, having to create new templates and defining what fields would be passed along.
This allows you to keep your current templates.

cheers!


How to dynamically render a field? (ajax / without reloading page)
#2

Good stuff!

It works pretty damn well for being quick and dirty. I like how pjax fallsback to hard loading if you disable javascript or if your browser doesn’t support the History API. Everything still works normally, like it should.


#3

With straight-up jQuery you can do this as well - this is how I have been building my AJAX sites:

$('.ajax-link').click(function(e) {
  e.preventDefault();
  var href = $(this).attr('href');
  loadLink(href)
})

function loadLink(url, nostatechange) {
  // this will load only the #main-container div on whatever page it requests
  // note the space in the quoted string below. URL should look like '/about #main-container'
  var loadUrl = url + " #main-container";
  // you can load straight into the body, but I like to have a master 'load' div to load into.
  // the load function will empty the div before loading new info into it
  var loadArea = $("#load");
  loadArea.fadeTo('400', '0', function() {
    loadArea.load(loadUrl, function () {
      // callback after loading 
      if (!nostatechange) { updateState(url) };  // see the popstate function below
      loadArea.fadeTo('400', '1');
    })
  })
}

plus a bit of URL & history updating:

// updates the browser's URL and places a new item in the history
function updateState(url) {
  var rootstring = window.location.origin;
  var newurl = url.replace(rootstring,'');
  history.pushState({}, "page", newurl);
}

// this handles back & forward in the browser. It updates the URL and the history,
// so we tell our loadLink function to not update the state afterwards
$(window).on('popstate', function(e) {
  loadLink(window.location.href, true);
})

It’s not as quick and dirty, but allows for a lot of customization. For instance, you could update a menu:

// about template, which I request and load from the homepage:
<div id="main-container" data-page="about">
  <div id="page-contents">
  ...
  </div>
</div>

// place the call for this function in the loadLink callback noted above
function updateMenu() {
  var active = $('#main-container').attr('data-page');
  $('.menu-items').removeClass('active');
  $('.menu-item-' + active).addClass('active');
}

More at jQuery docs: http://api.jquery.com/load/

(edit: fixed some formatting that was making the popstate function hard to read)


#4

This is great! thank you it works very well so far.

I’m having one issue however. Any idea how to get it to play nice with kirby’s routing options? It seems to ignore routing until the page is reloaded, that is, if the url was pushed via pjax in the first place.

I’m using the routing option from kirby’s documentation:

c::set('routes', array(
    array(
    'pattern' => '(:any)',
     'action'  => function($uid) {

       $page = page($uid);

       if(!$page) $page = page('blog/' . $uid);
       if(!$page) $page = site()->errorPage();

       return site()->visit($page);

     }
   ),
   array(
     'pattern' => 'blog/(:any)',
     'action'  => function($uid) {
       go($uid);
     }
   )
 ));

thanks!


#5

It’s ignoring the routes because it’s Javascript that is changing the URL - it never goes server-side, and never sees the routes. In this sense, it’s totally disconnected and even arbitrary (you could do an AJAX call without changing the URL, or update it to whatever you wanted).

You’d need to build a similar routing function into your javascript to generate the URLs. Or, if you are working with a minimal number of links, just set a data-url="/my-blog-post" alongside the href='/blog/my-blog-post. use the data-url string with the Update State.

Or, why don’t you set your anchor tags to simply href='/my-blog-post'?


#6

:astonished:

    $(document).pjax('a', '#www', {
        fragment: '#www',
        timeout: 2000
    });

    $(document).on('pjax:start', function() { ... });
    $(document).on('pjax:end', function() { ... });

#7

Well I found out that it was ridiculous hard to get a fade in / fade out transition working,
this is my solution :grimacing:

var transitions = "transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd";

    function pjaxify(url) {
        $('#intro').addClass("hidden").on(transitions, function(e) {
            $.pjax({
                url: url,
                container: "#www",
                fragment: "#www",
                timeout: 2000
            });
            $(this).off(e);
        });
    }

    if ($.support.pjax) {
        $(document).on('click', 'a[target!="_blank"]', function(event) {
            pjaxify(this.getAttribute('href'));
            event.preventDefault();
        })
    }

    $(document).on('pjax:start', function() { "Start your loader" });
    $(document).on('pjax:end', function() { "Reload you scripts (masonry etc.)" });

#8

Yo, i did this, if you want a clean approach.
Create a file in site/plugins, call it whatever…
Create a simple function in the plugin file:

function getPageUrl($page) {
  return str_replace('blog/', '', $page->url());
}

Call in in your template:

<a href="<?php echo getPageUrl($page) ?>">

:astonished: Im using a plugin in file to create all sorts “helpers” functions to keep the template files clean… Just a tip sir !


#9

With Kirby 2, it can be implemented even more elegantly:

Create a model for the page in site/models/blog.php that contains the following content:

<?php

class BlogPage extends Page {
  public function url() {
    return str_replace('blog/', '', parent::url());
  }
}

And in the template:

<a href="<?php echo $page->url() ?>">

#10

Sweet man, didn´t know about models, now I do, cool :stuck_out_tongue:!
hmm this function do replace the default function for url() ,right ? I can just perhaps
rename the public function to “whatever” and use that instead, to keep the original url function intact if I still need it for some reason ?


#11

Yep, sure. You can read more about page models in the Kirby documentation.


#12

hmm is all models template independent, I have to create a new model file for every template I have ? In that case I find it more useful to have all helper functions in a plugin file and be able to call them everywhere…


#13

You normally only have two templates that relate to the blog. One is the blog overview template and one the article detail template.
If you create models for these two templates, every page inside your blog will get these new methods, even if the currently displayed page has a different template. You don’t need to add the method to every template’s model for this (actually you shouldn’t, because models are specific to the template that they are defined for and not to the template that uses them).


#14

Ok cool thanks for feedback and help !! :thumbsup:


#15

Hi.
Everything is working so far. Great work! But I dont want to reload the main section. I want to load the new content in a seperate div in the middle of my page. It also works fine, but after the loading process the scroll position jumps back to top. Is there a way to prevent that? (I used the way of carlosbronze from top)


#16

Ok. Solved. The way of SQBiz dont have this scroll problem.


#17

I have a question about .active, in this case it adds the class to each menu item except when on home page (site.com/). Say the navigation is: Home, Projects, Contact
@ carlosbronze, how would the class be added to Home?