Support for Drupal 7 is ending on 5 January 2025—it’s time to migrate to Drupal 10! Learn about the many benefits of Drupal 10 and find migration tools in our resource center.
Problem
Currently, Drupal's forms only contain an ID attribute. This is a problem because:
- This is the only "styling hook" we have from a CSS standpoint and styling against ID's is generally not a best practice.
- 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.
- 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.
Comment | File | Size | Author |
---|---|---|---|
#34 | drupal8.form-default-css-class.34.patch | 2.99 KB | sun |
#32 | drupal8.form-default-css-class.32.patch | 2.36 KB | sun |
#27 | form-name-as-class_5.patch | 2.49 KB | Everett Zufelt |
#21 | form-name-as-class_4.patch | 2.54 KB | Everett Zufelt |
#19 | form-name-as-class_3.patch | 2.55 KB | Everett Zufelt |
Comments
Comment #1
aspilicious CreditAttribution: aspilicious commentedsub.
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 :).
Comment #2
Dave ReidNot sure what this has to do with html5 nor an actual bug. :/
Comment #3
aspilicious CreditAttribution: aspilicious commented@davereid this is needed for the html5 css cleanup. So it's related to the html5 initiative.
Comment #4
JacineI 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
Comment #5
Dave ReidWell there wasn't any explanation at all how it relates to HTML5...
Comment #6
JacineYou'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.
Comment #7
Everett Zufelt CreditAttribution: Everett Zufelt commentedPossibly / somewhat related #1244338: Add generic #class property for form elements
Comment #8
Everett Zufelt CreditAttribution: Everett Zufelt commentedI 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:
Comment #9
Everett Zufelt CreditAttribution: Everett Zufelt commentedI 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.
Comment #10
aspilicious CreditAttribution: aspilicious commentedThis is incorrect *i think* because forms with numbered id's: #form-2 will not get the .form class.
Comment #11
Everett Zufelt CreditAttribution: Everett Zufelt commented@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():
Comment #12
JacineJust 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:
Comment #13
Everett Zufelt CreditAttribution: Everett Zufelt commentedRemoved 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.
Comment #14
aspilicious CreditAttribution: aspilicious commentedthis possible brakes some themes if they used the added classes.
We should check for those.
Comment #15
Everett Zufelt CreditAttribution: Everett Zufelt commentedIn 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'.Comment #16
Everett Zufelt CreditAttribution: Everett Zufelt commented@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.
Comment #18
Everett Zufelt CreditAttribution: Everett Zufelt commentedSee #1285364: Remove search-block-form template for form rendering consistency
Comment #19
Everett Zufelt CreditAttribution: Everett Zufelt commentedUpdating 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.
Comment #20
stevetweeddale CreditAttribution: stevetweeddale commentedTagging for the current sprint
Comment #21
Everett Zufelt CreditAttribution: Everett Zufelt commentedSame as #19, but properly using drupal_html_class() to generate class name.
Comment #22
Everett Zufelt CreditAttribution: Everett Zufelt commentedNow 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.
Comment #23
JacineOk, 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.
Comment #24
Everett Zufelt CreditAttribution: Everett Zufelt commentedThe $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?
Comment #25
sunRegarding #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.This should go into drupal_prepare_form() or drupal_retrieve_form(), so it can be changed by hook_form_alter() implementations.
Comment #26
JacineThis 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
.I completely disagree with making
form.form-contact-site-form
orform.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.Comment #27
Everett Zufelt CreditAttribution: Everett Zufelt commentedRe-rolled patch in #21, incorporating input from #25, moving the addition of the class from form_builder() to drupal_prepare_form().
Comment #28
sunAlright, 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.
Comment #29
Everett Zufelt CreditAttribution: Everett Zufelt commentedSo, 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.
Comment #30
JacineI think it needs to be overridable. Otherwise, it'd be a bit of a WTF.
Comment #31
Everett Zufelt CreditAttribution: Everett Zufelt commentedSomeone 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.
Comment #32
sunAttached patch implements the default CSS class name in drupal_retrieve_form(), and additionally adds the base form ID as class name, too.
I like.
Comment #33
JacineThanks sun! :)
We should replace "suggestion" with "class." Suggestion is a loaded term and might cause confusion.
Is there a reason why we aren't using drupal_html_id() here? I'm guessing there is, but I'm curious.
I think this class is useful and should be kept.
0 days to next Drupal core point release.
Comment #34
sunAs 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.
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().
Comment #35
JacineUgh, 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']);?>
.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.
Comment #36
sunNote 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.
Comment #37
JacineTagging for this sprint.
Comment #38
cosmicdreams CreditAttribution: cosmicdreams commentedIs testing all that is left with this patch?
Comment #39
JacineThis 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:
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.
Comment #40
sun"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.
Comment #41
JacineOkay, fine. That's reasonable. :)
Comment #42
chx CreditAttribution: chx commentedI 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.
Comment #43
sun@chx: already mentioned 6 comments before ;) #798062: Rename TYPE_node_form to node_form_TYPE
Comment #44
JacineRemove sprint tag.
Comment #45
catchLooks good to me, committed to 8.x.