Add custom method to each block item in collection

On my services page, I have a services field which contains service blocks. Each service block has an icon, title and text field. These are displayed on the services page with an anchor link that’s generated from the title.

On my home page, I wish to display the services with each service having a link to the anchor on the services page.

How can I add a custom method for outputting the anchor URL to each service block in the services field?

Ideally I’d like to put it in a collection so that it can be used elsewhere.

Home controller

<?php

return function () {
	$serviceBlocks = page('services')?->services()->toBlocks();

	$services = new Collection($serviceBlocks);

	for ($i = 1; $i <= count($services); $i++) {
		$anchorUrl = page('services')->url() . '#' . Str::slug($services->nth($i)->content()->title());
		$services->nth($i)->content()->append($anchorUrl);
	}

	return [
		'services' => $services
	];
};
$services    = $page->services()->toBlocks()->toArray(); 
$newServices = [];
foreach( $services as $service) {
  $service['content']['anchorUrl'] = page('services')->url() . '#' . Str::slug($service['content']['title']);
  $newServices[] = $service;
}
dump($newServices);

Then you can create a new Blocks collection from that data.

However, if you want a collection that you can use anywhere, you have to create a collection in the collections folder or register the collection in a plugin. Defining a collection in the controller doesn’t help.

1 Like

Thank you but is there any possibility that it could be an object instead of an array?

In my home template, I want to loop through the services and access the anchorUrl like as if it’s a field.

<?= $service->anchorUrl() ?>

There is an idea about custom blocks methods: https://kirby.nolt.io/228

As I said, you can then convert the array into a (blocks) collection again. Or you can try map() on the blocks collection.

Could you please provide an example of how to do that?

Another approach to think about: not very clean looking, but should work.

<?php
// site/models/services.php

use Kirby\Cms\Page;
use Kirby\Cms\Field;

class ServicesPage extends Page {
  public function services() {
    $arr = json_decode($this->content()->get('services'), true);

    foreach($arr as &$item) {
      $item['content']['anchorUrl'] = 
          $this->url() . '#' . Str::slug($item['content']['title']);
    }

    return new Field($this, 'services', json_encode($arr));    
  }
}

In a template:

<ul>
  <?php foreach(page('services')->services()->toBlocks() as $service) : ?>
    <li>
      <a href="<?= $service->anchorUrl() ?>">
        <?= $service->title()->html() ?>
      </a>
    </li>
  <?php endforeach ?>
</ul>

You could even do this:

//replace return statement in the model function with:
return Blocks::factory($arr, [ 'parent' => $this ])->filter('isHidden', false);

in the template (remove the toBlocks() call):

<ul>
  <?php foreach(page('services')->services() as $service) : ?>
    <li>
      <a href="<?= $service->anchorUrl() ?>">
        <?= $service->title()->html() ?>
      </a>
    </li>
  <?php endforeach ?>
</ul>
1 Like

Thank you. I’ve used your code.

I found some minor issues though

$arr = json_decode($this->contents()->get('services'), true);

$this->contents() doesn’t exist. It should be $this->content()

Just make sure to put use Kirby\Cms\Blocks; at the top of the file.

sure, I’ve written it from the top of my head.
I’ve meant content() :slight_smile: