Problem

Currently, Drupal's forms only contain an ID attribute. This is a problem because:

  1. This is the only "styling hook" we have from a CSS standpoint and styling against ID's is generally not a best practice.
  2. ID's have a high CSS specificity. There are cases where Drupal core provides styles for form elements based on the form they are in. Because ID's are used in the CSS selectors, overriding the behavior in the theme layer requires (a) more specific selectors in order to win specificity wars and (b) unnecessary special-casing.
  3. ID's that are applied to forms are auto-generated and cannot safely be relied upon for theming purposes. If two of the same forms appear on the same page, you're left having to style both #form-1 and #form-2, which is far from efficient.

Proposed Solution

I'd like to see us add the name of the form as a class to use for styling purposes. I'm not asking to remove the ID's because they are still useful for other jobs, like quick DOM traversal and AJAX operations.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

aspilicious’s picture

sub.

We discussed this several times before and now it's time for doing it. I'm willing to TEST this when there is a patch ready :).

Dave Reid’s picture

Category: bug » feature
Issue tags: -html5

Not sure what this has to do with html5 nor an actual bug. :/

aspilicious’s picture

@davereid this is needed for the html5 css cleanup. So it's related to the html5 initiative.

Jacine’s picture

Category: feature » bug

I would call CSS completely failing when there are more than 2 of the same form on a page a bug.

It's good to know I can't even use my own initiative's tag. :s

Dave Reid’s picture

Issue tags: +html5

Well there wasn't any explanation at all how it relates to HTML5...

Jacine’s picture

You're right. I'm sorry for getting snappy, just a tad frustrated with the issue queue today. I realize this isn't ideal because technically it's not HTML5-related, even if it is happening under the initiative. I am going to try and figure out a way to do this better today.

Everett Zufelt’s picture

Everett Zufelt’s picture

I think we can do this in form_builder(), but there might be somewhere better, I'm not sure if form_preprocess_form() will work, it doesn't currently exist.

In form_builder we can add:

if (isset($element['#type']) && $element['#type'] == 'form) {
  $element['#attributes']['#classes'][] = $form_id;
}
Everett Zufelt’s picture

Status: Active » Needs review
FileSize
563 bytes

I haven't tested this, but I believe it will omit numbering in the class name if the same form is rendered twice on a page.

aspilicious’s picture

This is incorrect *i think* because forms with numbered id's: #form-2 will not get the .form class.

Everett Zufelt’s picture

@aspilicious

I think the unique (multiple forms) id is stored in $element['#id'] even when #type = form. Leaving $form_id untouched.

In drupal_prepare_form():

  if (!isset($form['#id'])) {
    $form['#id'] = 
drupal_html_id
($form_id);
  }
Jacine’s picture

Status: Needs review » Needs work

Just tested this and it doesn't actually work for the search block form. Dunno why. Everything else that I've looked at looks fine.

I also found 11 cases where forms add classes. Most of them are for 'user-info-from-cookie' and to supply a common class for contact "site" form vs. "user", etc. I did find 2 instances where the classes are duplicate (search) or so close (node) that it doesn't make sense to keep them:

// line 984 of search.module
$form['#attributes']['class'][] = 'search-form';

// line 114 node.pages.inc
if (!empty($node->type)) {
  $form['#attributes']['class'][] = 'node-' . $node->type . '-form';
}
Everett Zufelt’s picture

Status: Needs work » Needs review
FileSize
1.73 KB

Removed code mentioned in #12 , which causes duplicate classes on two forms. I have no idea what is going on with search-block-form (it is garbage), so may try again later if noone has fixed.

aspilicious’s picture

this possible brakes some themes if they used the added classes.
We should check for those.

Everett Zufelt’s picture

Status: Needs work » Needs review

In search.module"

1. the search block content is from drupal_get_form('search_block_form');
2. This is preprocessed in template_preprocess_search_block_form() where all form children are rendered into $variables['search_form']
3. in search-block-form-tpl.php $search_form is printed.

Nowhere is the $form parent rendered / printed, yet in the generated markup <form ...> ... </form> is wrapped around the markup generated from search-block-form.tpl.php.

So, where is <form ...> being rendered and printed?
Edit:

I believe that <form ...> is being added because the search-block-form render element is 'form'.

  return array(
    'search_block_form' => array(
      'render element' => 'form',
      'template' => 'search-block-form',
    ),
Everett Zufelt’s picture

@aspilicious

The only classes removed were two that were manually addd to node form and search form, but that will now be added automatically by form_builder(), so no change to markup.

Status: Needs review » Needs work

The last submitted patch, form-name-as-class_2.patch, failed testing.

Everett Zufelt’s picture

Everett Zufelt’s picture

Status: Needs work » Needs review
FileSize
2.55 KB

Updating failing test in #13. search_block_form will not get a class until #1285364: Remove search-block-form template for form rendering consistency is fixed.

Re: changing class names, it is true that the classes on node/add/x will change from 'node-[machine-name]-form' to '[machine-name]-node-form'. E.g. from 'node-page-form' to 'page-node-form'. I searched and nothing in Core is referring to the old class names.

stevetweeddale’s picture

Tagging for the current sprint

Everett Zufelt’s picture

FileSize
2.54 KB

Same as #19, but properly using drupal_html_class() to generate class name.

Everett Zufelt’s picture

Now that #1285364: Remove search-block-form template for form rendering consistency is committed the patch in #21 will add the form name as the class on every form in Core.

Jacine’s picture

Status: Needs review » Needs work

Ok, so this is working great on my end, but thinking about it a little more, there's one thing that may end up coming back to bite us that I didn't think of before. The problem arises when the node type is "page." The pattern we use here conflicts with the body classes, one of which is .page-node-TYPE:

BEFORE
.node-page-form

AFTER: Conflicts with the pattern of body classes.
.page-node-form

In addition to the pattern inconsistency, if someone creates a content type named "form" there would be a direct clash. I'd love not to special case this, but I think we probably should prevent the auto-generated class from being added and keep the original classes for this one case.

I'm curious what @sun thinks, so I will ask him to chime in.

Everett Zufelt’s picture

The $form_id of node forms is set in node_forms(). It is set as $type . 'node_form'.

We could:

1. Change the pattern for node form ids, and refactor the code that touches them.

2. Special case the node forms in form_builder() where the patch adds the class.

If we do 2 then can someone tell me what the most reliable way is to determin if an element passed into form_builder() is a node form?

sun’s picture

Regarding #23, what about simply adding a 'form-' prefix?

(Note that not all form constructor function names end in _form; that's a "best practice", but not enforced anywhere; it's perfectly valid to have form constructor function name of foo_bar(), which would turn into .foo-bar with the current patch, and .form-foo-bar with the prefix proposal.)

That said: We are, inevitably, going to run into class name clashes, no matter how hard we try.

Even the "form-" prefix is used by various other form-related things throughout core. Due to that, the only safe thing to do when actually using these classes in CSS is form.foo-bar, regardless of "form-" class prefix or not.

And to directly account for the already expected CSS selector specificity shitstorm: No, there are commonly only a few forms on a page, so the performance difference between form.foo-bar and .foo-bar is negligible.

+++ b/includes/form.inc
@@ -1711,6 +1711,8 @@ function form_builder($form_id, &$element, &$form_state) {
+  $element['#attributes']['class'][] = drupal_html_class($form_id);

This should go into drupal_prepare_form() or drupal_retrieve_form(), so it can be changed by hook_form_alter() implementations.

Jacine’s picture

Regarding #23, what about simply adding a 'form-' prefix?

This is an option, but it has a bigger WTF factor IMO because of all the other places the form prefix is used, and also because many forms end with "form" in their name, so this would give: .form-contact-site-form.

Due to that, the only safe thing to do when actually using these classes in CSS is form.foo-bar, regardless of "form-" class prefix or not

I completely disagree with making form.form-contact-site-form or form.contact-site-form some sort of rule for writing CSS in core. I think that's a shining example of us dong it wrong, and will fight it tooth and nail this entire development cycle, if I have to.

It would obviously be preferable avoid the common patterns, but we don't know of even one actual clash that exists in core. Unless there is a specific case where a clash is identified and core CSS needs to be written in that case, there's no reason to suggest CSS be written this way at all. IMO it's a front end DX and maintenance issue, and it wrongly introduces a higher than needed specificity and thereby imposes it onto the theme developer. It's a bad habit more than anything in Drupal, and not cool.

Since special casing the node forms is not wanted, and we don't actually have any actual clashes here, I'm leaning toward leaving it be $element['#attributes']['class'][] = drupal_html_class($form_id);. We can make a note in the documentation of this change about how it works and let others decide how they want to proceed in their themes. That's way better than imposing higher specificity on people for an edge case.

Everett Zufelt’s picture

Status: Needs work » Needs review
FileSize
2.49 KB

Re-rolled patch in #21, incorporating input from #25, moving the addition of the class from form_builder() to drupal_prepare_form().

sun’s picture

+++ b/includes/form.inc
@@ -944,6 +944,7 @@ function drupal_prepare_form($form_id, &$form, &$form_state) {
+  $form['#attributes']['class'][] = drupal_html_class($form_id);

+++ b/modules/node/node.pages.inc
@@ -111,9 +111,6 @@ function node_form($form, &$form_state, $node) {
-  if (!empty($node->type)) {
-    $form['#attributes']['class'][] = 'node-' . $node->type . '-form';
-  }

Alright, thanks.

We need to discuss the expected and behavior a bit more.

With the class being added in drupal_prepare_form(), the form constructor is not able to "prevent" it, because drupal_prepare_form() is invoked afterwards.

If we want form constructors to override the default suggestion - for which node_form() could be an example (but not necessarily) - then we'd have to pre-set the class in drupal_retrieve_form(), right before the initial $form array is created and passed to the constructor. That way, the constructor (and only the constructor) is able to completely replace $form['#attributes']['class'] with a new/custom definition, throwing away the default class name.

I think that would be the desired behavior/workflow.

If we are going to do it this way, then the line adding the class in drupal_retrieve_form() will need an inline comment explaining why it is done there and not in drupal_prepare_form() (i.e., basically what I outlined above, only shorter ;))

1 days to next Drupal core point release.

Everett Zufelt’s picture

So, the question is if we want to supplement the class(es) provided by the form constructor with drupal_html_class($form_id), or if we want to have drupal_html_class($form_id) be the default, which can be completely overridden by the form constructor.

Jacine’s picture

I think it needs to be overridable. Otherwise, it'd be a bit of a WTF.

Everett Zufelt’s picture

Someone else is going to need to roll against drupal_retrieve_form. I took a look and I don't see where to add the class. It appears like an array merge is required (which makes me wonder why we don't just do that in drupal_prepare_form(). It also seems like adding a class is more of a 'prepare' behavior than a 'retrieve' behavior.

sun’s picture

Assigned: Unassigned » sun
Category: bug » task
FileSize
2.36 KB

Attached patch implements the default CSS class name in drupal_retrieve_form(), and additionally adds the base form ID as class name, too.

I like.

Jacine’s picture

Status: Needs review » Needs work

Thanks sun! :)

+++ b/includes/form.incundefined
@@ -762,6 +762,15 @@ function drupal_retrieve_form($form_id, &$form_state) {
+  // form constructor function to override or remove the suggestion.

We should replace "suggestion" with "class." Suggestion is a loaded term and might cause confusion.

+++ b/includes/form.incundefined
@@ -762,6 +762,15 @@ function drupal_retrieve_form($form_id, &$form_state) {
+    $form['#attributes']['class'][] = drupal_html_class($form_state['build_info']['base_form_id']);

Is there a reason why we aren't using drupal_html_id() here? I'm guessing there is, but I'm curious.

+++ b/modules/node/node.pages.incundefined
@@ -110,10 +110,9 @@ function node_form($form, &$form_state, $node) {
-  $form['#attributes']['class'][] = 'node-form';

I think this class is useful and should be kept.

0 days to next Drupal core point release.

sun’s picture

Status: Needs work » Needs review
FileSize
2.99 KB

Is there a reason why we aren't using drupal_html_id() here? I'm guessing there is, but I'm curious.

As the name implies, drupal_html_class() is for CSS classes, drupal_html_id() is for HTML IDs. The former ensures minimal requirements for the resulting string only, the latter combines the HTML and CSS specs to ensure a universally valid and unique HTML identifier.

I think this class is useful and should be kept.

The base form ID is "node_form", and this patch automatically takes the base form ID of all forms into account (if any) and adds them as second default CSS class.

I.e., all node forms still have the 'node-form' class. And not only node forms, but also comment forms and other $form_ids that are re-using the same base form constructor automatically get the respective class name of the base form ID. (This is actually the point where this patch really starts to make sense ;))

See http://api.drupal.org/api/search/8/forms for implementations of hook_forms() -- namely, comment_forms(), node_forms(), and search_forms().

Jacine’s picture

As the name implies, drupal_html_class() is for CSS classes, drupal_html_id() is for HTML IDs. The former ensures minimal requirements for the resulting string only, the latter combines the HTML and CSS specs to ensure a universally valid and unique HTML identifier.

Ugh, I'm so sorry... For some reason I looked at that and saw <?php $form['#attributes']['id'][] = drupal_html_class($form_state['build_info']['base_form_id']);?>.

The base form ID is "node_form", and this patch automatically takes the base form ID of all forms into account (if any) and adds them as second default CSS class.

I.e., all node forms still have the 'node-form' class. And not only node forms, but also comment forms and other $form_ids that are re-using the same base form constructor automatically get the respective class name of the base form ID. (This is actually the point where this patch really starts to make sense ;))

That's awesome and how it should be! Yay! This stuff is totally confusing, so thank you so much for taking the time to explain it. :)

The patch looks good, but gonna leave as needs review for a few, so I can do some testing.

sun’s picture

+++ b/modules/node/node.pages.inc
@@ -110,10 +110,9 @@ function node_form($form, &$form_state, $node) {
+  // Override the default CSS class name, since the user-defined node type name
+  // in 'TYPE-node-form' potentially clashes with third-party class names.
+  $form['#attributes']['class'][0] = drupal_html_class('node-' . $node->type . '-form');

Note that this special case is subject to change for Drupal 8 already:

#798062: Rename TYPE_node_form to node_form_TYPE

The current code is a namespace violation on its own. To be discussed and changed over there.

Shouldn't hold up this patch.

Jacine’s picture

Issue tags: +D8H5

Tagging for this sprint.

cosmicdreams’s picture

Is testing all that is left with this patch?

Jacine’s picture

Status: Needs review » Needs work
Issue tags: -D8H5 +sprint

This looks good aside from that bloody search form block. The CSS for the search modules targets the ".search-form" class. This patch removes that class, and leaves us with:

  • In the block: search-block-form, search-box
  • On the search page: search-form

So, what do we do about this? I think it makes sense to keep the common class here, personally. Marking needs work based on that for now.

sun’s picture

Status: Needs work » Needs review

"search_box" is completely outdated and old code either way. Dates back to earlier core versions, in which themes contained a $search_box variable, which in turn wasn't a block, but a box.tpl.php (also gone), etc.

Totally whack form structure: http://api.drupal.org/api/drupal/modules--search--search.module/function...

Today, we only have the form in the block (search_block_form) and on the page (search_form).

Due to search_forms(), the form ID of the block is search_block_form, but it calls into search_box instead of search_form. Hence, the form in the block gets the .search-block-form and .search-box classes.

Actually, I think that figuring out appropriate classes for theming and rewriting those inane search forms is a separate issue. This patch does not change the CSS classes being output on search forms, aside from automatically adding the form IDs as classes (which applies to all forms, not limited to search forms). If we want to review and adjust the automatically added classes for all forms throughout core, then we will still review this patch in a year from now.

Jacine’s picture

Status: Needs review » Reviewed & tested by the community

Okay, fine. That's reasonable. :)

chx’s picture

I reviewed this and it's fine but i would love to see a followup issue opened to change foo_node_form to node_form_foo finally. It's totally against the rules.

sun’s picture

@chx: already mentioned 6 comments before ;) #798062: Rename TYPE_node_form to node_form_TYPE

Jacine’s picture

Issue tags: -sprint

Remove sprint tag.

catch’s picture

Status: Reviewed & tested by the community » Fixed

Looks good to me, committed to 8.x.

Automatically closed -- issue fixed for 2 weeks with no activity.