Dropdown Drivel: Two New Form Fields in DOM

21 Jun

Dropdown Drivel: Two New Form Fields in DOM

Whenever I’m working on a project that needs some sort of custom form field, I usually do my best to build it in a really modular way so that it can be reused. If it seems especially useful and I can imagine others enjoying it, I roll it into the great dumping ground we call DataObjectManager.

I’m not particularly proud of this, but it is a major benefit of having a huge install base for my work. It’s really easy to deploy useful things without asking you all to download new code from new places. As some of you may know, the “code” folder of DOM has already been segmented, somewhat logically, into submodules including dropdown fields, wysiwyg fields, and date fields. Today we’re welcoming to new members of the dropdown fields family — LiveDropdownField and FilteredDropdownSet.

LiveDropdownField

I was motivated to create this when I saw not only how taxing, but also how cumbersome SimpleTreeDropdownField was becoming. We love SimpleTreeDropdownField for the obvious benefits:

  • Working within a DOM popup (we shun the likes of TreeDropdownField for its marriage to the Prototype library)
  • Snappy response time (no AJAX calls)

But SimpleTreeDropdownField can become an enemy, given the right context. If you have 400 pages in your site tree, the database overhead is tremendous. Further, if you have really long page titles, the formatting gets really ugly. This is something we’ve worked on to some extent. Community members Dan Hensby and Marcus Dalgren in particular have been strong contributors to the improvement of SimpleTreeDropdownField.

Performance enhancements aside, SimpleTreeDropdownField can be a really frustrating way to locate a page when you have hundreds in your result set. LiveDropdownField allows you to perform an autocomplete search of the result set to find the page you’re looking for, and also comes with an option to expose the entire result set with one click. Here’s what it looks like right now:

Here’s how you make it work.

    static $has_one = array(
        "Link" => "SiteTree"
    );

    public function getCMSFields() {
        $fields = new FieldSet(
            new LiveDropdownField("LinkID", "Link to page", "SiteTree")
        );
        return $fields;
    }

It’s pretty straightforward! Here’s the API:

  1. The name of the form field. (Don’t forget to add “ID” if this is for a has_one relation)
  2. The title, or label, for the form field
  3. The data class to search (defaults to “SiteTree”)
  4. The field to use as the label for your results. This is also the field that will be searched with a LIKE clause during the autocomplete query. Defaults to “Title”

That’s it! We could use some improvements in the UI, so feel free to contribute:

https://github.com/unclecheese/DataObjectManager/blob/master/code/dropdown_fields/LiveDropdownField.php

FilteredDropdownSet

This widget is a little more complex, and much less likely to be used, if you ask me, but it’s come up a few times in my projects, so I figured it was time to bundle something up. Sometimes dropdown fields are sympathetic to each other, that is, the choices in one are dependent on the selected choice of one of its peers. Imagine that we’re choosing a product category and a product. Products have a CategoryID. A workflow might look like:

  • Choose a category (Fruit, Vegetable, Meat, Grain)
  • User chooses “Fruit”
  • A second dropdown, “Choose a product” is now populated with “Apples, Oranges, Pears”

Let’s look at a datamodel for that configuration:

Product.php

	static $db = array (
		'Title' => 'Varchar(255)',
		'Description' => 'Varchar(255)',
	);

	static $has_one = array (
		'ProductCategory' => 'ProductCategory',
	);

AvailableProduct.php

	static $has_one = array (
		'ProductCategory' => 'ProductCategory',
		'Product' => 'Product'
	);

Some of you data geeks might have picked up on the the redundancy of the ProductCategoryID field. After all, if a Product has one Category, and we know the product, don’t we inherently know the Category as well? Of course. It is absolutely redundant. But unfortunately, for this release, it’s a requirement in order to preserve the state of the dropdowns  after saving.

Here’s how it will look:

Now let’s look at implementation:

                $category_map = array();
                $product_map = array();
                if($result = DataObject::get("ProductCategory")) {
                    $category_map = $result->toDropdownMap();
                }
                if($result = DataObject::get("Product")) {
                    $product_map = $result->toDropdownMap();
                }

		$fields->push(new FilteredDropdownSet(array(
			new DropdownField('ProductCategoryID', _t('Mysite.PRODUCTCATEGORY','Category'), 'ProductCategory',$category_map),
			new DropdownField('ProductID', _t('Mysite.PRODUCT', 'Product'), 'Product', 'Title'$product_map)
		),"ProductCategoryID","Product"));

And here’s our API:

  • An array of the dropdown fields to be included in the FieldGroup (inline display of form fields)
  • The field that is being filtered on the dropdown on the right (defaults to “ParentID”)
  • The data class for the objects being filtered (defaults to “SiteTree”)

In theory, the list of dropdowns can be infinitely long, provided the filtered field and data class are always the same, for instance, if you wanted to traverse the site tree using dropdowns. I’m not sure that’s a practical use of this field, but it would work. Most of the time, a pair of dropdowns will serve your needs.

Enjoy!

https://github.com/unclecheese/DataObjectManager/blob/master/code/dropdown_fields/FilteredDropdownSet.php

13 Responses to “Dropdown Drivel: Two New Form Fields in DOM”

  1. lx 21. Jun, 2011 at 6:13 pm #

    These fields look very usefull, especially the LiveDropdownField if you have thousends of entries.
    Thanks UC!

  2. francisco 21. Jun, 2011 at 10:19 pm #

    this new fields are amazing Aaron.
    thanks for sharing :)

  3. Tim 23. Jun, 2011 at 11:52 am #

    Thanks Aaron, the FilteredDropdownSet is exactly what I was looking for. I’m having trouble getting it to work in ModelAdmin though.

    My objects has_ones are creating DropDownFields automatically and I want the FilteredDropdownSet to replace them. Not having any luck.

    • unclecheese 23. Jun, 2011 at 1:45 pm #

      Hi, Tim,

      These dropdown fields are not part of the scaffolding that comes out of the box. You’ll need to use a getCMSFields() function like you do with SiteTree objects, but just don’t call parent::getCMSFields().

      • Tim 27. Jun, 2011 at 10:56 am #

        I did have the new field in a getCMSFields() function. I’m not understanding what you mean when you say don’t call parent::getCMSFields(). If I take that out then what do I replace it with. $fields = ?what?

        • Aram 27. Jun, 2011 at 11:50 am #

          Do:

          $fields = new FieldSet();

          That gives you an empty field set to add fields to, however it also means you will need to build your own tabs if you need them like so:

          $fields = new FieldSet(
          $rootTab = new TabSet(“Root”,
          new TabSet(‘Content’)
          )
          );

  4. Andrew Houle 23. Jun, 2011 at 12:35 pm #

    Love this dude!

    For the LiveDropdownField it would be sweet if the user could choose if it’s an outgoing or internal link before the sitetree menu appears, somewhat similar to the main CMS WYSIWYG.

    • unclecheese 23. Jun, 2011 at 1:50 pm #

      Hey, Andrew! Thanks for the feedback. Internal vs. external links are business logic that isn’t really in the scope of the form field. LiveDropdownField is just used to browse for related has_one objects. In this case, I used SiteTree as an example. In a real use case, you’d probably see two fields, one for internal (has_one) link, and one for external ($db) link. I’ll typically write a function like:

      function MagicLink() {
      return ($this->ExternalLink && trim($this->ExternalLink) != “http://”) ? ‘href=”{$this->ExternalLink” target=”_blank”‘ : ‘href=”{$this->InternalLink()->Link()}”‘;
      }

      |a| $MagicLink>click me|/a|

      ^ Strip tags workaround..

  5. Tero Hietanen 30. Nov, 2011 at 3:36 pm #

    I’ve been trying to use FilteredDropdownSet and I’ve got it listing the categories and items correctly and the selection is saved correctly, but when editing, the dropdowns don’t select the saved item. I suppose it has to do with that AvailableProduct.php thing, but how exactly is the class and the has_ones in it supposed to be named?

    • Tero Hietanen 14. Dec, 2011 at 4:22 am #

      Never mind, I finally got it. The redundant category field is in the same object, where the FilteredDropdownSet is.

  6. mauricio 04. Mar, 2012 at 9:41 pm #

    i can’t get this to work (filteredDropdown):( i’m so frustrated! please can you explain it, from scratch! i don’t get what i have to do and all my test gone wrong:/ i’ll appreciate your help!

  7. Tobias Oetiker 16. Aug, 2012 at 5:13 am #

    I am trying to use the LiveDropdownField in a widget, but I keep getting

    Fatal error: Call to a member function FormAction() on a non-object in /home/itis-web/public_html/sapphire/forms/FormField.php on line 98

    when I try to use the widget. Below is a cut down version
    of what I am trying to achieve.

    class UpcomingEventsHomeWidget extends Widget {
    static $has_one = array(
    ‘ShowEventsFrom’ => ‘SiteTree’
    );
    public function getCMSFields() {
    return new FieldSet(
    new LiveDropdownField(‘ShowEventsFromID’,’Show events from’,’SiteTree’),
    );
    );
    }

  8. DimiStripe 06. Sep, 2012 at 11:17 am #

    UC – is there an easy way of redoing your FilteredDropdownSet element into dropdown with ComplexTable for picking has_many elements ?

    In my situation i need to pick a category and then check checkboxes in ComplexTable with Subcategories for this exact category.

    Maybe you can guide me towards the solution for this in 2.4 ?