How to do groups of tags

Silverstripe Version: 4.1

Context is a page that lists products. There’s four pages of a type that will list their products.

Each page needs it’s own groups of tags used to filter the products.

The requirement is to arbitrarily create both the groups and the tags/filters inside the groups.

I realise it would be far easier (and better) if the groups were fixed and just the tags were flexible but that’s not going to cut it for this project.

Is a composite field of title and a select2 tag based input the way to go for this? Better approach?

I made some progress and got most of this working.

My question is the area in pink in the attached pic.

I need to be able to apply the Filter Group and it’s child Filters to a Product.

Assuming I need to do something like this in Product::getCMSFields().


// $page = Get this Product's page

// $FilterGroups = $page get it's Filter Groups


foreach ($FilterGroups as $Group)
{
    // Create a display field for $Group->Title   

    // Create a bunch of checkboxes per of the Filters to be applied to Products
    $fields->addFieldToTab('tab name', CheckboxSetField::create('Filters', 'Filters', $Group->Filters->map('ID', 'Title')))
} 

Anyone able to fill in the blanks?

It might be worth mentioning the silverstripe-tagfield module

Thanks for that Robbie,

I had seen it and implemented it - works great for a known tag list but doesn’t really help me with what I’m trying to do.

I can’t see a way to arbitrarily create new TagFields (not the tags themselves) for example as they have to be related to a DataObject.

If a UI helps: http://pes.haricot.co/wayne

Filter group is cars and phones. Filters/tags are grouped beneath. Problem is users need to be able to create groups and tags without me predefining them.

If I could create more of these (Pretend “Product Tags” says “Cars”:

At run time that would help a lot. I could create “Phones” for example. If I could template that with a label to output “Cars” or “Phones” and clicking create added a new set of label and TagField that would be super.

Gah so close…

Progress… I did this in the Product::getCMSFields:


# Get the FilterGroups for the related page to this Product.
$FilterGroups = FilterGroup::get()->filter('ProductListPages.ID', $this->record['ProductListPagesID']);

foreach ($FilterGroups as $Group)
{
    # Create a TagField for each Group with it's Filters
    $fields->addFieldToTab('Root.Filters', TagField::create(
      $Group->Title,
      $Group->Title,
      $Group->Filters()
    )
    ->setShouldLazyLoad(true)
   ->setCanCreate(false));
}

That get’s me just the TagFields for the FilterGroups applied to the related product page.

Now the problem is I can’t save these.

On Product I have:

private static $has_many = array(
    "Filters" => Filter::class
);

Which is fine if I have one Filter but of course I have a variable amount and I set them with the group name (which I don’t know what it’ll be), Cars for example. I get an error BadMethodException Method Cars doesn’t exist on Product.

Make sense… The method doesn’t exist.

How can I interrupt that? I’d hoped to use an onBeforeWrite to remove that data and save it myself with onAfterWrite but it spits that error before I can get to it.

I’m not sure if I’m following this properly, but it sounds to me like you need a GridField. Your Page has_many (or many_many if you want to reuse them on different pages) FilterGroups, and your FilterGroup has a many_many relation to Tag.

In the getCMSFields() of your Page you would set up a GridField for editing the FilterGroups relation. That’s probably all you would need to do, because the nested gridfield for linking Tags to each FilterGroup would be scaffolded automatically. But you could override the scaffolding to use a TagField if you wish.

Hey @JonoM,

Sort of. The relationship between Groups and Filters (the left side of the diagram) is handled pretty easily with a GridField on FilterGroups for it’s Filters and going the other way, a select on a Filter for it’s one FilterGroup.

Those Groups are then added to a page with a list box. Probably change this to a TagField even though they’re basically the same with createable=false. Tried to get away from GridField here as it’s a cumbersome interface and users really just need to tag FilterGroups to a Page.

Products are then managed on a Page with another GridField. The gap was the big pink area above, a Product needed to have access to only the GroupFilters it’s related page had.

That’s where I ran into a host of problems. I needed to display different TagFields for each applicable FilterGroup with the applicable Filters for that Group.

I did find a way to do it.

In Product::getCMSFields:


# Get the FilterGroups as applied on the related ProductListPage
if (array_key_exists('ProductListPagesID', $this->record))
{
	$FilterGroups = FilterGroup::get()->filter('ProductListPages.ID', $this->record['ProductListPagesID']);

	foreach ($FilterGroups as $Group)
	{
		# Create a TagField for the FilterGroup
		$fields->addFieldToTab('Root.Filters', TagField::create(
			$Group->Title,
			$Group->Title,
			$Group->Filters()
		)
			->setShouldLazyLoad(false)
			->setCanCreate(false)
		);
	}
}
else
{
	$fields->addFieldToTab('Root.Filters', LabelField::create('Before adding any filters, please select a Product List Page to associate this Product with and click save.'));
}

Gets:
g2

To get around the problem previously posted of “BadMethodException Method Cars doesn’t exist on Product”. I added:

public function defineMethods()
{
    parent::defineMethods();

    $FilterGroups = FilterGroup::get();

    foreach ($FilterGroups as $Group)
	{
		$this->addWrapperMethod($Group->Title, 'FilterWrapper');
	}
}	

So that’ll wrap up any FilterGroup created and send it to “FilterWrapper” method. No more errors.

public function FilterWrapper($GroupTitle)
{
    $Group = FilterGroup::get()->filter(['Title' => $GroupTitle])->first();

    return $this->Filters()->filter(['ProductsID' => $this->ID, 'FilterGroupID' => $Group->ID]);
}

And that’ll apply the values selected for each FilterGroup.

The last problem was saving a Product. TagField seems to only want to save two ID’s but I need three, Product.ID FilterGroup.ID and the Filter.ID. So in Product::onAfterWrite:


public function onAfterWrite()
{
	parent::onAfterWrite();

	$store = [];

	# Get all the Filters and their group in a flattened array
	foreach (Filter::get() as $Filter)
	{
		$store[$Filter->ID] = $Filter->FilterGroupsID;
	}

	# Update each applied Filter row to include it's parent FilterGroup
	foreach ($this->Filters() as $Filter)
	{
		$this->Filters()->add($Filter, ['FilterGroupID' => $store[$Filter->ID]]);
	}
}

I update the Product → Filter relationship to include the FilterGroup.ID so I can find the Filters to apply to the correct FilterGroup when editing a Product.