Load More with AJAX

I’ve attempted to follow the K2 Load more with AJAX tutorial but haven’t been able to get it to work. I get the error when I load my project page: Call to undefined method Kirby\Http\Request::ajax()

Is there something I need to be doing differently to get it to work in K3?

Grateful for any help as I have little experience with AJAX.

Thanks!

There is no replacement for the AJAX check in Kirby 3, see this discussion on GitHub: https://github.com/getkirby/kirby/issues/1431

I think the solution would be to use a second controller for the json template.

Hmmm I see. I hate to be a pain, but I’m not sure I follow what I’d need to do to get it working? How would a second controller for json template work?

It gets the name of the template, in this example, projects.json.php. And you would put in the code from within the ajax check.

So I’d create a controller called projects.json.php and have it set up like this? (Sorry just a stab in the dark lol)

<?php

  if(r::ajax() && get('offset') && get('limit')) {

    $offset = intval(get('offset'));
    $limit  = intval(get('limit'));

    $projects = $projects->offset($offset)->limit($limit);

    $more = $count > $offset + $limit;


  } else {

    $offset   = 0;
    $limit    = 2;
    $projects = $projects->limit($limit);

  }

  return compact('offset', 'limit', 'projects', 'more');

};

I’m also looking to implement load more using Ajax and am having the same issue. How would the two controllers work?

Your controller is not complete, have a look at the docs for the syntax of a controller. Also, the if (r::ajax) statement has to disappear from the if statememt. But you have to define the $projects and $morevariables.

@jjh Did you work out a functioning controller for this? I’m also struggling with implementing this tutorial with K3.

Hi @texnixe

I’m struggling to get my head around this: is this now not possible in Kirby because of the deprecated request? Or is there a simple workaround with an additional controller that I’m missing? Thanks!

The solution is to use a content representation that is called from your AJAX request instead of using the same template/controller for both.

I’m struggling to understand what that means. Any examples you can share?

Before we had content representations, we had to use the standard PHP template to handle both the standard HTML request and the requests coming from an AJAX call, therefore the need for the if statement with the if(r::ajax() condition.

But since Kirby 2.ican’trememberthe version, we have content representations, that allow you to output the content in different formats.

While the example Load more recipe still uses the if statement, you can in fact handle the logic in the if statement in a separate controller for the representation instead.

So you would end up with 4 files instead of three:

  • projects.php template
  • projects.json.php template
  • projects.php controller
  • projects.json.php controller

The logic you need for the projects.json.php template needs to go into the projects.json.php controller.

1 Like

I got this working, and actually understand why/how! Thanks!

Hey @jamiehunter, could you share how you implemented with me?

Sure. I’ll base it on the K2 example for you:

/site/templates/projects.php

<ul class="projects" data-page="<?= $page->url() ?>" data-limit="<?= $limit ?>">

  <?php foreach($projects as $project): ?>
    <?php snippet('project', compact('project')) ?>
  <?php endforeach ?>

</ul>
<button class="load-more">Load more</button>

/site/snippets/project.php

<li class="project">
  <a href="<?= $project->url() ?>" class="project-link">
    <?php if($image = $project->image()): ?>
    <img src="<?= $image->url() ?>" alt="<?= $image->alt_text()->html() ?>" />
    <?php endif ?>
    <div class="project-caption">
      <h2 class="project-title"><?= $project->title()->html() ?></h2>
    </div>
  </a>
</li>

/site/controllers/projects.php

<?php

return function($site, $pages, $page) {

  $projects = $page->children()->visible();
  $count    = $projects->count();
  $more     = false;

  $offset   = 0;
  $limit    = 2;
  $projects = $projects->limit($limit);

  return compact('offset', 'limit', 'projects', 'more');

};

/site/templates/projects.json.php

<?php

$html = '';

$projects = $page->children()->visible();
$count    = $projects->count();

$offset = intval(get('offset'));
$limit  = intval(get('limit'));

$projects = $projects->offset($offset)->limit($limit);
$more = $count > $offset + $limit;

foreach($projects as $project) {

  $html .= snippet('project', compact('project'), true);

}

$data['html'] = $html;
$data['more'] = $more;

echo json_encode($data);

/site/controllers/projects.json.php

<?php

return function ($kirby, $site, $pages, $page) {
  
  $projects = $page->children()->visible();
  $count    = $projects->count();

  $offset = intval(get('offset'));
  $limit  = intval(get('limit'));

  $projects = $projects->offset($offset)->limit($limit);

  $more = $count > $offset + $limit;

  return [
      compact('offset', 'limit', 'projects', 'more')
    ];
};

JS

$(function () {
  var element = $(".projects");
  var url = element.data("page") + ".json";
  var limit = parseInt(element.data("limit"));
  var offset = limit;

  $(".load-more").on("click", function (e) {
    $.get(url, { limit: limit, offset: offset }, function (data) {
      if (data.more === false) {
        $(".load-more").hide();
      }

      element.children().last().after(data.html);

      offset += limit;
    });
  });
});

This should work. Let me know if you have any problems!

2 Likes

Thank you so much for this! Got it working! The only issue I noticed was the use of visible() instead of listed()

1 Like

Ah yes, a hangover from the old cookbook. I use a collection to get my articles.

1 Like

Hi @jamiehunter I’ve been trying to replicate your solution but I can’t seem to get any of it to work. Any thoughts on how to debug?

/site/templates/interviews.php

<?php foreach($interviews as $interview): ?>
  <?php snippet('single-interview', [
    'interview' => $interview,
    'size' => 'default'
  ]) ?>
<?php endforeach ?>

<button class="load-more">Load more</button>

/site/controllers/interviews.php

return function ($page, $pages) {

  $identifier = $page->template(); // uniquely identiy page by type

  $interviews = $pages->find('interviews')->children()->flip()->listed();
  $count = $interviews->count();
  $more = false;

  $offset = 0;
  $limit = 2;
  $interviews = $interviews->limit($limit);

  return compact('identifier', 'offset', 'limit', 'interviews', 'more');

};  

/site/templates/interviews.json.php

$html = '';

$interviews = $pages->find('interviews')->children()->flip()->listed();
$count    = $interviews->count();
$offset = intval(get('offset'));
$limit  = intval(get('limit'));

$interviews = $interviews->offset($offset)->limit($limit);
$more = $count > $offset + $limit;

foreach($interviews as $interview) {

  $html .= snippet('
    single-interview',
    [
      'interview' => $interview,
      'size' => 'default'
    ],
    true
  );

}

$data['html'] = $html;
$data['more'] = $more;

echo json_encode($data);

/site/controllers/interviews.json.php

return function ($kirby, $site, $pages, $page) {
  
  $interviews = $pages->find('interviews')->children()->flip()->listed();
  $count    = $interviews->count();

  $offset = intval(get('offset'));
  $limit  = intval(get('limit'));

  $interviews = $interviews->offset($offset)->limit($limit);

  $more = $count > $offset + $limit;

  return [
      compact('offset', 'limit', 'interviews', 'more')
    ];
};

assets/js/interviews.js

$(function() {
  const element = $('.interviews');
  const url = `${element.data('page')}.json`;
  const limit = parseInt(element.data('limit'));
  let offset = limit;

  $('.load-more').on('click', function(e) {
    console.log(url);
    $.get(url, { limit, offset }, function(data) {
      if (data.more === false) {
        $('.load-more').hide();
      }

      element
        .children()
        .last()
        .after(data.html);

      offset += limit;
    });
  });
});

It’s definitely not correct to have the logic in the controller and in the template?

Yeah, totally makes sense.

If I keep the logic in the controller, the.json page throws an error

Undefined variable: interviews

when its in the template it runs fine but the json file is empty.