Form control: One-to-many configurator

The oneToManyConfigurator control is rather like a hybrid of the One-to-many Select and the Many-to-many Select form controls. It allows you to link objects as with a many-to-many join, but also to add extra extra information that further defines each specific join.

These two scenarios will give you an idea of where you would use a one-to-many configurator:

Scenario 1

You are running an event management system. You have an event_ticket object and an event_session_category object. A ticket will give you a defined quota of sessions from different categories. So, you effectively want a many-to-many join between the two objects, while also recording how many sessions from the linked category are allowed by that particular ticket.

Scenario 2

You have a library of image assets, which you want to link to an article object. But when you link an image, you want to specify whether it is the master image for that particular article, and maybe also override the image's default title and caption.

Arguments

formName (required) The name of the form to be used to configure the object. Can also be defined as an annotation on the configurator object, in which case it may be omitted.
labelRenderer (required) The label renderer to be used to generate the label text to display in the form control. Can also be defined as an annotation on the configurator object, in which case it may be omitted.
fields (optional) A comma-separated list of fields on the main form which should have their values passed through to the configurator form.
targetFields (optional) A comma-separated list of fields on the configurator form that the fields defined above should be mapped to. If omitted, the fields names will be the same on both forms.
multiple (optional) True of false (default). Whether or not to allow multiple record selection
sortable (optional) True or false (default). Whether or not to allow multiple selected records to be sortable within the control. Note that you will explicitly need to define a sort_order property on your configurator object.

Example

First, let's set up our configurator Preside object:

// /preside-objects/event_ticket_session_category.cfc

/**
 * @nolabel
 * @oneToManyConfigurator
 * @labelRenderer           event_ticket_session_category
 * @configuratorFormName    preside-objects.event_ticket_session_category.configurator
 */
component  {
	property name="event_ticket"           relationship="many-to-one" relatedTo="event_ticket"           required=true;
	property name="event_session_category" relationship="many-to-one" relatedTo="event_session_category" required=true;

	property name="allowance"  type="numeric" dbtype="int";
	property name="sort_order" type="numeric" dbtype="int";
}

A few things to note here:

  • Both objects to be linked are set as having many-to-one relationships.
  • We have specified @nolabel as the label for this object will be generated by the label renderer
  • The configurator object must have the @oneToManyConfigurator annotation
  • @labelRenderer defines the label renderer to be used to build the labels
  • @configuratorFormName is the form definition to be used by the form control to create the link

The relationship to this object is defined on the event_ticket object, just like a normal one-to-many relationship:

// /preside-objects/event_ticket.cfc
...
property name="session_categories" relationship="one-to-many" relatedTo="event_ticket_session_category" relationshipKey="event_ticket";
...

We then set up the field in the event_ticket form definitions. Note that we have omitted formName and labelRenderer attributes, as they are defined on the configurator object. Also, control="oneToManyConfigurator" is not strictly necessary, but it makes it easier to remember that the configurator form control will be used.

By specifying fields="eventId", we are saying we want the eventId value from this form to be passed through into eventId on the configurator form. This will often not be needed.

<!-- /forms/preside-objects/event_ticket/admin.edit.xml -->
<!-- /forms/preside-objects/event_ticket/admin.add.xml -->
<!-- ... -->
<field binding="event_ticket.session_categories" sortorder="30" control="oneToManyConfigurator" fields="eventId" />

Screenshot of the empty configurator form control

Now we define the configurator form:

<!-- /forms/preside-objects/event_ticket_session_category/configurator.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<form>
	<tab id="default" >
		<fieldset>
			<field binding="event_ticket_session_category.event_session_category" sortorder="10" required="true" filterBy="eventId" filterByField="event_id" />
			<field binding="event_ticket_session_category.allowance"              sortorder="20" />

			<field binding="event_ticket_session_category.event_ticket"           sortorder="30" control="hidden" />
			<field name="eventId"                                                 sortorder="40" control="hidden" />
		</fieldset>
	</tab>
</form>

This form will be loaded by Ajax, and will display two fields: an object picker to let you choose the session category, and a field for the category allowance.

Note the two hidden fields. The event_ticket field is automatically populated with the id of the ticket record from which we came. You will always need to include this field. The eventId field accepts the value we passed through from the calling form, and can then be used by the event_session_category object picker to filter the choices displayed.

Screenshot of the configurator form

Finally, we need to tell our configurator how to construct labels for the selected options. In this case, we want the name of the selected category, followed by the allowance specified (or "unlimited" if it is left blank).

To do this, we will use Preside's new label renderers.

// /handlers/renderers/labels/event_ticket_session_category.cfc

component {

	private array function _selectFields( event, rc, prc ) {
		return [
			  "allowance"
			, "event_session_category"
			, "event_session_category.label as __event_session_category_label"
		];
	}

	private string function _renderLabel( event, rc, prc ) {
		var allowance            = arguments.allowance                      ?: "";
		var sessionCategoryId    = arguments.event_session_category         ?: "";
		var sessionCategoryLabel = arguments.__event_session_category_label ?: renderLabel( "event_session_category", sessionCategoryId );
		var label                = "#sessionCategoryLabel#: ";

		if ( len( allowance ) ) {
			label &= allowance;
		} else {
			label &= "unlimited";
		}

		return label;
	}

}

This is covered in more detail in the label renderers guide.

The _selectFields() method defines the fields required in order to render the label server-side (i.e. when a saved record is being displayed), and the _renderLabel() method takes thos fields and actually builds the label.

However, it now works slightly differently when using a one-to-many configurator. All the data from the configurator form is passed into _renderLabel() in the arguments scope. But the form only knows about the id of the selected session category, and not its name. So we need to add in an extra piece of logic which will get the label text from the event_session_category object if it's not present in the arguments scope.

Screenshot of the configurator form control with rendered labels

Info

Note that any selections you make via the One-to-many Configurator form control are only saved when you save the parent record - in this case the event_ticket - even though it may look a bit like the QuickAdd functionality.