5

I'm working on a really simple module in Drupal 8 to get the hang of things. What it does (or rather, what the page I'm asking about in this question does) is grab some data of a bunch of simple article-style entries, and display them.

What I would like to be able to do is to have it only display the first few (say, 5), and then have a "Read more" button, which uses AJAX to display the next 5, lengthening the page so it now shows 10.

Here is the controller as I have it at the moment. The routing.yml file points to MyController::build. For the sake of simplicity, I've left out exactly how it gets things from the database, and some simple processing (in particular, image gets converted from a managed_file in the database into a URL to the image).

class MyController extends ControllerBase {

  public function build() {
    $stuff = $this->getStuff();
    return array(
      '#theme' => 'my_theme',
      '#stuff' => $stuff,
      '#attached' => array(
        'library' => array(
          'my_theme/style',
          'my_theme/readmore',
        ),
      ),
    );
  }

  private function getStuff() {
    $stuff = getStuffFromDatabase();
    $result = array();
    foreach($stuff as $key => $item) {
      $tmp = new \stdClass();
      $tmp->id = $item->id;
      $tmp->title = $item->title;
      $tmp->description = $item->description;
      $tmp->image = $item->image;
      $result[] = $tmp;
    }
  }
}

Which then gets sent to a template file:

<section>
  <ul>
    {% for item in stuff %}
      <li>
        <h3><a href="stuff/{{ item.id }}">{{ item.title }}</a></h3>
        {% if item.image is not empty %}
          <a href="stuff/{{ item.id }}">
            <img src="{{ item.image }}" />
          </a>
        {% endif %}
        {% if item.description is not empty %}
          <p>{{ item.description }}</p>
        {% endif %}
      </li>
    {% endfor %}
  </ul>
</section>
<form method="post" action="" onsubmit="readmore()">
  <button name="read-more" type="submit">Read more</button>
</form>

What I want is a way to change this set up so that at first the controller only sends a few of the entries, but when the button is pushed, it sends the next few, and the template displays the new ones as well as the old.

Thanks


Okay, I think I may have worked out to at least make a start on this problem. I've followed this tutorial, and want to get a small message displaying at the bottom, to at least get a feel for how Ajax works with Drupal 8. I've added

<div id="current-msg">
  <h2></h2>
  <p></p>
</div>

to the bottom of the .html.twig file.

I created a file identical to his ReadMessageCommand.php, except named mine MyMessageCommand.php (and renamed the class inside it accordingly). The function render has myMessage in place of readMessage.

I added a method called myMessageCallback into MyController, which works the same as in that guide, but with my_module_load_message and MyMessageCommand in the appropriate places.

To readmore.js, I added the function from the above guide, but with readMessage replaced by myMessage. I also added the following, just so I can know more easily if the function is at least being called.

console.log(response.subject);
console.log(response.content);

And I added the core/drupal.ajax dependency to the readmore library.

My question is, how do I actually get this function to be called. As far as I can tell, the guide that I've used has no indication of it. How to I get a message to be displayed in the div at the bottom? Once I've figured that out, I feel extending it so that the button acts as a "read more" shouldn't be too much trouble.

  • Have a look at AjaxResponse and AppendCommand. Use those in your controller method. You also need to store the current page for the query, I'd use data- attribute on the read more link. Your JS should pick this value and send it to the controller when the link is clicked so that you can use pagination in the query and return only the requested "page" form the query. Also the hide/show the read more link based on available results, so add InvokeCommand in your controller to trigger hide/show jquery on that link based on this. – user21641 Jan 28 '16 at 7:32
  • @IvanJaros So, I could store the most recent page by setting data-page-number="0", but where in the page would that go, exactly? On the <ul>? And I'm afraid I don't understand how to go about using AjaxResponse and AppendCommand. – Jim Cullen Jan 28 '16 at 7:58
0

That tutorial won't help you. That's for more advanced stuff.

I would not use a form, but a simple link with an ajax response.

But if you want to use a form you need to create a proper Drupal form. Too much to write here to explain that so just take a look here https://www.drupal.org/node/2117411

Your buildForm should be something like

/**
 * {@inheritdoc}
 */
public function buildForm(array $form, FormStateInterface $form_state) {
  // The default value for working with non JS should be taken from the route and adding 1.
  $form['page_number'] = array(
    '#type' => 'hidden',
    '#default_value' => 0,
  );
  $form['my_stuff'] = array(
    '#type' => 'value',
    '#value' => [],
  );
  $form['actions']['#type'] = 'actions';
  $form['actions']['submit'] = array(
    '#type' => 'submit',
    '#value' => $this->t('Read more'),
    '#button_type' => 'primary',
    '#ajax' => array(
      'callback' => 'Drupal\example\Controller\MyController::myFormSubmit',
    ),
  );
  return $form;
}

You can call this form in your page with (changing accordingly):

$form = \Drupal::formBuilder()->getForm('Drupal\example\Form\ExampleForm');

You do all the stuff you want in your submitForm, and not in the callback if you want the form to work without JavaScript. If you don't care you can do in the ajax callback. I will assume you do it in the submitForm.

/**
 * {@inheritdoc}
 */
public function submitForm(array $form, FormStateInterface $form_state) {
  $page = $form_state->getValue('page_number');
  $stuff = getStuffFromDatabase($page);
  $result = array();
  foreach($stuff as $key => $item) {
    $tmp = new \stdClass();
    $tmp->id = $item->id;
    $tmp->title = $item->title;
    $tmp->description = $item->description;
    $tmp->image = $item->image;
    $result[] = $tmp;
  }
  $page++;
  // Here for non JS you probably would just redirect to the proper page
  $form_state->setValue('page_number', $page);
  $form_state->setValue('my_stuff', $result);
}

Then in your MyController::myFormSubmit

use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\AfterCommand;
use Drupal\Core\Ajax\HtmlCommand;
use Drupal\Core\Ajax\InvokeCommand;

public static function myFormSubmit(array $form, FormStateInterface $form_state) {
  $response = new AjaxResponse();
  $selector = $form_state->getValue('my_stuff');

  // Here you build the render array based on the output you want on the page
  $build = [
    '#prefix' => '<div>',
    '#suffix' => '</div>',
  ];
  // The selector is as if you did this in jQuery
  $response->addCommand(new AfterCommand(
    '.some-selector',
    $build
  ));
  $response->addCommand(new HtmlCommand(
    '.some-selector-that-wraps-the-form',
    $form
  ));
  return $response;
}

That's the gist of it. It should put you on the right track.

If you want to use a simple link. You need your initial function to take that into account and send what you have now for non JS and a response as in the ajax submit callback when it's called through JS.

Note that if you add a class "use-ajax" to a link and write the link as "/some/path/nojs" Drapal.Ajax will change that to "/some/path/ajax" so its easy to know if the called was made through Ajax or not.

  • I'm not married to the idea of using a form. I was only trying to do it that way because it seemed the most obvious way. How would you go about it without a form? And what's wrong with the linked tutorial? It all seemed pretty simple and easy to follow. Seems like it's just missing one last step. – Jim Cullen Jan 29 '16 at 2:47
  • Because it doesn't seem to be possible to comment on the edit review process, @KrishnaMohan, did you even bother reading the edit‽ In what possible way is it "intended to address the author of the post"‽ All I did was correct what was a minor typo which would have resulted in a syntax error. There's no way that needed to involve a comment. – Jim Cullen Jan 29 '16 at 6:40
  • The problem with the linked tutorial is that it tells you how to create commands like the ones I used. IE HtmlCommand. What you want is to actually use those commands and you can achieve what you want with the commands provided by core. If you want to do more complex stuff, feel free to do so, but you should learn how to use them. For using a link instead of a form you need to create a route for /my-page/{js}/{page}. Then use the link as /my-page/nojs/X with a class of "use-ajax". In your controller check if $js == 'nojs' return array, else return AjaxResponse. – tic2000 Jan 29 '16 at 11:52
  • The "missing step" in that blog is all I wrote here, which can be a blog post in itself. – tic2000 Jan 29 '16 at 11:59

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.