Layout field: Don't allow empty rows/columns

Hi there! I am a seasoned WordPress Developer and am trying to re-create the ACF flexible content field with Kirby’s layout field, to create a simple page builder.

So far, it’s working great except for one thing: I can’t find a way to disallow empty rows in the panel. Basically I’m looking for a way to make filling in at least one block per row/column mandatory:

Is there a way to achieve that? Making layouts required?

Ok, an empty layout doesn’t look nice in the Panel, however, in reality it doesn’t matter because you can just ignore it on the frontend. So this is basically a visual issue, I think.

There is no built-in way to enforce adding blocks to a layout. One thing you could do is use a page.update:before hook and check if the fields contains empty layouts and throw an error.

@texnixe Thanks for the hint with page.update:before! I was able to solve it like this in site/config/config.php:

return [
	'hooks' => [
		'page.update:before' => function (Kirby\Cms\Page $page, array $values) {

			// Bail early if this is not a project
			if ($page->intendedTemplate()->name() !== 'project') return;
			// Bail early if there are no details (the layout field's name)
			if (empty($values['details'])) return;

			// Parse $details
			$details = json_decode($values['details']);

			foreach ($details as $row) {
				if (allColumnsAreEmpty($row->columns)) throw new Exception('Please delete empty rows');
			}
		}
	]
];

/**
 * Checks if all columns in a Layout row are empty
 */
function allColumnsAreEmpty(array $columns): bool
{
	foreach ($columns as $column) {
		if (count($column->blocks)) return false;
	}
	return true;
}

I first tried to use Kirby’s API but got lost since I couldn’t find how I could cast $values['details'] to a layout field. Any pointers there? Something like this maybe?

toField('layout', $value);

Is details the field name?

In that case I’d rather works with $page->details()->toLayouts()

So basically, you could check if you have any empty layouts:

$hasEmptyLayouts = $page->details()->toLayouts()->filterBy('isEmpty, true')->count() > 0;

And if that variable returns true, throw an error.

Note that this will not return true, if the layout contains an empty block. But you could do an additional check in that case if you need.

Yes, details is my layout field.

I also first tried validating the field from $page, but it seems like that doesn‘t catch newly added rows. It will run on the current layout field, ignoring any changes that are about to be saved, doesn‘t it?

You are right! However, you can convert the field value to a new field object and then continue as outlined above:

$field = new \Kirby\Cms\Field($page, 'details', $values['details']);
$hasEmptyLayouts = $field->toLayouts()->filterBy('isEmpty, true')->count() > 0;

Aaah nice, that‘s what I was looking for! Thanks so much @texnixe :partying_face:

Is there some extensive documentation on the filterBy method in the docs somewhere? I couldn’t find any. This is what I ended up with instead:

return [
	'hooks' => [
		'page.update:before' => function (Kirby\Cms\Page $page, array $values) {

			// Bail early if this is not a project
			if ($page->intendedTemplate()->name() !== 'project') return;
			// Bail early if there are no details
			if (empty($values['details'])) return;

			$field = new \Kirby\Cms\Field($page, 'details', $values['details']);
			foreach($field->toLayouts() as $layout) {
				if (allColumnsAreEmpty($layout->columns())) throw new Exception('Please delete empty rows');
			}
		}
	]
];

/**
 * Checks if all columns in a Layout row are empty
 */
function allColumnsAreEmpty(Kirby\Cms\LayoutColumns $columns): bool
{
	foreach ($columns as $column) {
		if ($column->blocks()->count() > 0) return false;
	}
	return true;
}

In my case, I only want to prevent completely empty layouts. So if at least one column contains at least one block, it should pass.

A friend pointed me towards the filter compendium page. Using that, I was able to condense the above code to this:

return [
	'hooks' => [
		'page.update:before' => function (Kirby\Cms\Page $page, array $values) {

			// Bail early if this is not a project
			if ($page->intendedTemplate()->name() !== 'project') return;
			// Bail early if there are no details
			if (empty($values['details'])) return;

			$field = new \Kirby\Cms\Field($page, 'details', $values['details']);

			if ($field->toLayouts()->filterBy('isEmpty', '==', true)->count() > 0) {
				throw new Exception('Please delete empty rows');
			}
		}
	]
];

@texnixe Is the filterBy('isEmpty, true') you suggested some kind of short hand or a typo?

1 Like

Typo, the closing quote should be after isEmpty

filterBy('isEmpty', true)
1 Like