Kirby 3 Many To Many Field
This plugin allows you to create many-to-many relationships between pages in Kirby. The relationship is bidirectional, meaning it can be edited from either side and is automatically updated on the other side. The relationship can have attributes that can be updated from both sides as well. You can define multiple many-to-many relations on one page. If a page with a relation to one or many other pages gets deleted, all relations to this page get deleted as well.
You need to install the AutoId plugin by Bnomei to your project as well for this plugin to work.
This plugin uses two hooks: the page.update:after and the page.delete:before hook. If you use these hooks in your project as well, make sure to rename the hooks and trigger them seperately as described here.
Installation
Download
Download and copy this repository to /site/plugins/kirby3-many-to-many-field
.
Git submodule
git submodule add https://github.com/jonasholfeld/kirby3-many-to-many-field.git site/plugins/kirby3-many-to-many-field
Composer
composer require jonasholfeld/kirby3-many-to-many-field
Setup
- 3.0.1 Quickstart
- 3.1 Naming and Type
- 3.2 The foreignkey field
- 3.3 The unique validator
- 3.4 The relation fields
- 3.5 Corresponding blueprint
- 3.6 Additional structure fields
- 3.7 How to use in templates
1. Install AutoID
Add the AutoID plugin by Bnomei to your kirby-project.
2. Use AutoID to identify your pages
Both blueprints of the pages you want to have a many-2-many relation need to have this field:
autoid:
type: hidden
translate: false
The AutoID plugin automatically generates a unique ID for every page that will be created after it’s install. If some pages already exist without an ID, you can force a re-index.
3. Setup your blueprints
The many-to-many plugin gets all its information about the related pages from your blueprints, so it’s essential to set them up right. You can check out the example blueprints to get a better idea about how to setup yours.
Both blueprints need the manytomany field in order to connect the pages correctly. As it’s important to set them up correctly, the following text explains every step bit by bit.
3.0.1 Quickstart
You can use and adjust these two blueprints to setup a relation between two pages with the plugin. It implements the classic Employee <--> Project relation you might know from database examples (see ER-diagram above). Make sure to rename all fields according to your situation. To fully understand all the fields and adjust them to your situation you should read on.
project.yml
title: Project
fields:
description:
type: text
label: Description
employees:
type: manytomany
label: Employees
fields:
foreignkey:
label: Employee
type: multiselect
min: 1
max: 1
options: query
query:
fetch: site.find('employees').childrenAndDrafts
text: "{{ page.title }}"
value: "{{ page.autoid }}"
hours:
type: number
label: Number of hours
validate:
unique: employees
relatedPage: employees
relatationField: projects
autoid:
translate: false
employee.yml
title: Employee
fields:
age:
type: number
label: Age
projects:
type: manytomany
label: Projects
fields:
foreignkey:
label: Project
type: multiselect
min: 1
max: 1
options: query
query:
fetch: site.find('projects').childrenAndDrafts
text: "{{ page.title }}"
value: "{{ page.autoid }}"
hours:
type: number
label: Number of hours
validate:
unique: projects
relatedPage: projects
relatationField: employees
autoid:
translate: false
3.1 Naming and Type
You can name the field how you like. A name hinting to the nature of the relation or the templates of the related pages might be helpful.
You need to specify the type as manytomany:
myRelatedPages: #<-- name how you like
type: manytomany
...
The manytomany-field inherits from the structure field, so it is setup like a normal structure-field with a couple of additional fields that need to be filled.
3.2 The foreignkey field
The foreignkey field is the field inside our manytomany-field that saves the "foreign keys". In our case they are the IDs created by the autoID plugin. You create a field inside the manytomany-field called "foreignkey" that is a multiselect that queries its options from the pages you would like to link to. To be more specific, it queries the children of a given page, so you need to specify the name of the parent-page to whose subpages you would like to link to. It is important to use page.autoid as the value, you can chose what to use as the text, but I recommend to use page.title to identify the pages.
myRelatedPages:
type: manytomany
lable: My Related Pages
fields:
foreignkey: #<-- name needs to be *foreignkey*
label: Related Page
type: multiselect
min: 1
max: 1
options: query
query:
fetch: site.find('myRelatedParentPage').childrenAndDrafts # <-- use name of parent-page of related pages here
text: "{{ page.title }}"
value: "{{ page.autoid }}"
validate:
unique: myRelatedPages
...
3.3 The unique validator
Duplicate entries inside the manytomany field cause problems, so make sure to use the unique validator of the plugin:
myRelatedPages:
type: manytomany
lable: My Related Pages
fields:
foreignkey:
label: Related Page
type: multiselect
min: 1
max: 1
options: query
query:
fetch: site.find('myRelatedParentPage').childrenAndDrafts
text: "{{ page.title }}"
value: "{{ page.autoid }}"
validate:
unique: myRelatedPages #<-- use name of your field
...
3.4 The relation fields
There are three equally important fields you need to add to the manytomany field. They specify the template of the related pages, their parent-page and the name of the corresponding manytomany field in their blueprint. Make sure to fill them out correctly.
myRelatedPages:
type: manytomany
lable: My Related Pages
fields:
foreignkey:
label: Related Page
type: multiselect
min: 1
max: 1
options: query
query:
fetch: site.find('myRelatedParentPage').childrenAndDrafts
text: "{{ page.title }}"
value: "{{ page.autoid }}"
validate:
unique: myRelatedPages
relatedPage: myRelatedFolder #<-- name of the parent-page of the linked pages
relatationField: myOtherRelatedPages #<-- name of the corresponding manytomany-field in the blueprint of linked pages
...
3.5 Corresponding blueprint
To be able to edit the relation from both sides, both blueprints of the related pages need to have a field of the type manytomany. They need to have corresponding values in the specific fields. Here is an example of two blueprints, in this case with a relation between students and schools.
school.yml
title: School
fields:
students:
type: manytomany
label: Students
fields:
foreignkey:
label: Student
type: multiselect
min: 1
max: 1
options: query
query:
fetch: site.find('students').childrenAndDrafts
text: "{{ page.title }}"
value: "{{ page.autoid }}"
validate:
unique: students
relatedPage: students
relatationField: schools
autoid:
type: hidden
translate: false
student.yml
title: School
fields:
students:
type: manytomany
label: Students
fields:
foreignkey:
label: Student
type: multiselect
min: 1
max: 1
options: query
query:
fetch: site.find('students').childrenAndDrafts
text: "{{ page.title }}"
value: "{{ page.autoid }}"
year:
type: number
validate:
unique: students
relatedPage: students
relatationField: schools
autoid:
type: hidden
translate: false
Once your blueprints are setup like this, the manytomany field changes on both sides, when there is an update from one of them.
3.6 Additional structure fields
As mentioned above, the manytomany field is just a structure field with some special fields. That means you can add any number of fields to the structure, if you need to save some extra information about the relation, e.g. a year. Just make sure the two linked blueprints both have the extra fields in the manytomany field:
school.yml
title: School
fields:
students:
type: manytomany
label: Students
fields:
foreignkey:
label: Student
type: multiselect
min: 1
max: 1
options: query
query:
fetch: site.find('students').childrenAndDrafts
text: "{{ page.title }}"
value: "{{ page.autoid }}"
year: #<-- some extra field
type: number
validate:
unique: students
relatedPage: students
relatationField: schools
autoid:
type: hidden
translate: false
student.yml
title: Student
fields:
schools:
type: manytomany
label: Schools
fields:
foreignkey:
label: School
type: multiselect
min: 1
max: 1
options: query
query:
fetch: site.find('schools').childrenAndDrafts
text: "{{ page.title }}"
value: "{{ page.autoid }}"
year: #<-- the same extra field
type: number
validate:
unique: schools
relatedPage: schools
relatationField: students
autoid:
type: hidden
translate: false
3.7 How to use in templates
employee.php
Title: = $projectPage->title() ?>
Hours: = $project->hours() ?>
">Projects
// using the `toStructure()` method, we create a structure collection from the manytomany-field
$projects = $page->projects()->toStructure();
// we can then loop through the entries and render the individual fields
foreach($projects as $project):
// Fetching the project page by using the findBy-method on the parent-page of the related pages
$projectPage = page("projects")->children()->findBy("autoid", $project->foreignkey()->value()); ?>
<h2>Title: = $projectPage->title() ?>h2>
<h2>Hours: = $project->hours() ?>h2>
endforeach; ?>
License
MIT
Credits
- Jonas Holfeld
- Developed during my internship and with the friendly support of Christoph Knoth and Konrad Renner at Knoth & Renner