Creating Dynamic Tabs on Content Types

By mandclu, Fri, 04/14/2023 - 06:24
A multitude of tabs with indecipherable labels

Towards the end of 2021 I had an idea: what if Drupal could expose content in a grid, similar to what users might be used to in database GUIs like Sequel Pro or Sequel Ace? I was able to put together a simple proof of concept, and the Autogrid module was born.

I had originally wanted this to be available as a tab on relevant bundles (content types, taxonomy vocabularies, etc) but at the time I wasn't sure how to implement this, so I created a custom menu item where a user could manually choose a content entity type, then a bundle, and then see the grid. After all, as Seth Godin famously said in his book Linchpin, "Real artists ship."

I kept wanting to come back to my original vision, however, so I recently decided to invest some time into making it work. It took some investigation, and I couldn't find much documentation on the pieces required, so I'm writing this to make life easier for anyone looking to implement something similar, or in case future Martin ever wants to use this pattern again.

Step 1: Create Your Controller

The controller is the class that will actually generate the markup the user will see. As the name implies, it's generally where the business logic will live. In my case, I had defined a controller as part of Autogrid's initial development. 

Step 2: Define the Route

Next, we need to define the path where the user will interact with the controller. When I first created Autogrid, I defined a single path at which all grids could be generated in a YAML file, autogrid.routing.yml.

For the new implementation I wanted the path to look like a sibling to common bundle operations like managing fields, display, and form display. To do this dynamically I needed to create a RouteSubscriber. I realized that the Devel and Auto Entitylabel modules did something similar, so I examined their implementations (devel and ael). The key is to create a class that extends Drupal core's RouteSubscriberBase abstract class, with the alterRoutes method as the place where we actually provide new routes for every bundle that needs to make use of our functionality.

A couple of pieces made this work a little trickier for what I wanted to implement for Autogrid. Unlike the modules I was using as reference, I didn't want to just go ahead and generate the routes for every possible entity. I wanted site builders to pick the entities where this functionality would be useful, and only add routes for those. This, in turn, introduced the second complication: It seemed intuitive to have site builders choose the main entity types where autogrids would be useful: nodes, taxonomy terms, storage, and so on. But where we actually needed to add the new routes were on the associated bundle entity types: content types, taxonomy vocabularies, storage types, and so on.

In the implemented RouteSubscriber I iterated through the configured entity types instead of all available content entity types, and then if a bundle type is available, use that instead. It starts with an existing link template for the entity type (either the edit form, or the canonical link as a fallback), and then appends the /grid path token for where the controller will be accessed.

The last task is to register our RouteSubscriber as an event_subscriber in our services YAML file.

Step 3: Create the Local Task as a Deriver

You can define local tasks (which appear in the UI as tabs) in a YAML file within your module. In a simple use case this would explicitly detail the task to make available. Since we want to generate them dynamically we need to create another class and reference it in autogrid.links.task.yml. This time we will extend the DeriverBase abstract class, and again we can refer to implementation in our example modules (devel and ael).

Again, our implementation for Autogrid differs by starting with only the configured entity types, and then using the bundle type for each if it exists. Otherwise it is very similar, in using the getDerivativeDefinitions method to define the new local tasks, referencing the routes we defined in the previous step.

Step 4: Everything Else

The approach we've discussed above relies on a configuration form to allow the site builder to choose which entity types will have the new "Manage data" tab added on bundles. That involves creating a form class, specifying a route to the form, and then setting that route as the configuration task in our module's info.yml file. We also added a new permission for this form, so site builders have granular control over who can change where the grids are available.

Your own implementation will differ in what additional details are needed, but it's common for there to be a few extra pieces to make everything work seamlessly.

Step 5: Profit!

With the RouteSubscriber and local task Deriver in place, you should now be able to see your own dynamic generated task visible on entity bundles. Depending on the complexity of the sites where you will be using your new functionality, the list of local tasks can get long, so try to keep your label succinct. With great power comes great responsibility!

 

Comments2

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.

Selwyn Polit (not verified)

10 months 1 week ago

Nice article. I went looking for the link to autogrid.links.task.yml but it failed. I don't see the file in the source actually. Am I missing something?

mandclu

10 months 1 week ago

In reply to by Selwyn Polit (not verified)

Thanks for the feedback! The link has been updated.