Silverstripe Tip - Using Unsaved Relations in Gridfield Edit Forms

You have two related objects, and want to modify the edit form of one based on the properties of another. This is easy to accomplish once the objects, and the relationship joining them, have been written - but how do you achieve it before the two objects are aware of each other?


Scenario

As always, we’ll work with a real-world example. Let’s say you have a Page with a has_many relation to CarouselSlide, and each CarouselSlide has a title, description and attached image. As your site grows, it becomes harder to track which carousel images belong to which page, so you decide that each image should be uploaded into a folder that matches the page name.

This is easy, right? You just fetch the related page and set the folder name to match its URL segment:

// CarouselSlide.php
public function getCMSFields() 
{
	$fields = parent::getCMSFields();
	$imageField = $fields->dataFieldByName('Image');
	$imageField->setFolderName('carousel/' . $this->Page()->URLSegment);
}

Nope. While this appears to work, the next time you try to add a carousel slide you’ll notice that the folder name is simply “/carousel/”. This is because the object hasn’t been written yet, so $this->Page() is empty!

One (terrible) option is to hide, or disable, the UploadField until the object has been written, so you can guarantee that $this->Page() returns something useful. But we don’t want to settle for that…

GridField to the rescue!

Presenting GridFieldDetailForm::setItemEditFormCallback(). This handy function allows us to modify the edit form that the GridField builds from where we construct the GridField (in this case, the Page). Let’s see how it solves our problem:

// Page.php
public function getCMSFields() 
{
	$fields = parent::getCMSFields();

	$fields->addFieldToTab(
		'Root.Carousel',
		GridField::create(
			'CarouselSlides',
			'Carousel slides',
			$this->CarouselSlides(),
			$config = GridFieldConfig_RecordEditor::create()
		)
	);

	// Get the detail form component
	$detailForm = $config->getComponentByType('GridFieldDetailForm');
	// PHP 5.3 compatibilty :( otherwise just use $this and remove use($self) below
	$self =& $this;
	// Set the callback: a closure which accepts one parameter - the edit form
	$detailForm->setItemEditFormCallback(function($form) use ($self) 
	{
		// Get the image field from the form fields
		$imageField = $form->Fields()->dataFieldByName('Image');
		// Set the folder name, easy!
		$imageField->setFolderName('carousel/' . $self->URLSegment);
	});

	return $fields;
}

The callback is triggered after the item’s edit form has already been loaded, so we can work with whatever fields that the item’s getCMSFields() method adds.

Caveats

The only caveat to this method is that if we choose to add a new field, its value won’t be pre-populated (as this pre-population happens before the closure is called, so before the field exists - it can’t fill out a field that doesn’t exist yet!). Thankfully, it’s fairly simple to work around this:

$detailForm->setItemEditFormCallback(function($form) 
{
	// You might get the record that we're editing from the form data...
	$record = $form->getRecord();
	// ... then pass the record value into the field
	$form->Fields()->push(
		TextField::create('MyField', 'Field title', $record->MyField)
	);

	// Alternatively, add your fields without passing a value...
	$form->Fields()->push(
		TextField::create('MyField', 'Field title')
	);
	// ...then trigger loading the data when you've finished
	$mergeStategy = $record->ID == 0 ? Form::MERGE_IGNORE_FALSEISH : Form::MERGE_DEFAULT;
	$form->loadDataFrom($record, $mergeStategy);
});

Wrap-up

So there you have it: a clean API for modifying GridFields from whichever context you intend to use your model in. No URL hacks, no Session hacks, no Controller hacks, just organic closure goodness. Go forth and modify!

Published on

2nd April 2015
by Loz Calver

Filed Under

SilverStripe

How can we help?

Please send us some details of your requirements and we’ll be in touch.

Please note by submitting your details you are agreeing for Bigfork Ltd to store your data in order to process your enquiry and that you have read our Privacy Policy.