Backfill: adopt the global .form / .field form system in the new Angular Edit Content editor
@adrianjm-dotCMS is already working on this.
Since Sep 16, 2026.
- Dominant language
- Java
- Stars
- 970
- Forks
- 486
- Avg merge
- 3d 33m
- Merged PRs (30d)
- 170
Description
Description
The dotCMS form system is global CSS, not per-component styling. It lives in core-web/apps/dotcms-ui/src/style.css (lines 35-95) and is documented in docs/frontend/STYLING_STANDARDS.md:
.form { @apply w-full space-y-5; }
.form .field { @apply flex flex-col gap-1; }
.form .field > label { @apply text-sm font-medium; }
.form .form-checkbox,
.form .form-radio { @apply flex flex-row items-center gap-2; }
.form .form-checkbox label,
.form .form-radio label { @apply text-sm font-normal; }
.form .p-field-hint { @apply text-sm text-gray-500; }
.form .p-field-error { @apply text-sm text-red-500; }
.p-label-input-required::after { content: '*'; } /* added by the dotFieldRequired directive */
Every rule is descendant-scoped to .form. A form that omits class="form" silently loses the entire system — .field, label typography, hint and error styles all stop applying.
The new Angular Edit Content editor (core-web/libs/edit-content) never adopted it. Design reported it directly: "the edit content screen is not taking the classes from the .form > .field > label". dotCMS/dotcms-claude-plugins#33 added this rule to the automated code reviewer, so new code is now covered — this issue is the backfill of the existing editor.
Verified findings
| # | Finding | Location |
|---|---|---|
| 1 | Root <form> carries class="p-fluid h-full" — no .form, so every global rule below is inert across the whole screen |
libs/edit-content/src/lib/components/dot-edit-content-form/dot-edit-content-form.component.html:9 |
| 2 | <div class="field"> appears once in the entire library, and it sits under a <div class="p-fluid"> with no .form ancestor → dead class |
.../dot-edit-content-sidebar/components/dot-edit-content-sidebar-workflow/dot-edit-content-sidebar-workflow.component.html:75 |
| 3 | A parallel abstraction reimplements .field as <div class="flex flex-col gap-2"> inside a component, and puts the <label> inside another component |
libs/edit-content/src/lib/fields/dot-card-field/** (4 components, 18 consumers) |
| 4 | p-label-input-required is hand-written — it is the private output of the dotFieldRequired directive and must never be written by hand |
dot-card-field-label.component.html:3, dot-form-file-editor.component.html:47 |
| 5 | Visible bug: 7 field templates already use p-field-hint / p-field-error, but those classes only exist under .form and nothing defines them locally → hints and validation messages render unstyled today; errors are not red |
text, select, radio, checkbox, calendar, custom, relationship field templates |
| 6 | Checkbox/radio option rows hand-roll flex items-center gap-2 instead of the global .form-checkbox / .form-radio |
dot-edit-content-radio-field.component.html, dot-edit-content-checkbox-field.component.html |
| 7 | Labels carry typography/spacing classes (class="mb-2 inline-block"); site-field also points for at "language-field" — a copy-paste a11y bug |
.../dot-select-existing-content/components/language-field/language-field.component.html:1, .../site-field/site-field.component.html:1 |
| 8 | Component SCSS reimplements form layout (__form, __field, __error-slot) |
.../dot-edit-content-file-field/components/dot-form-file-editor/, .../dot-form-import-url/ |
Approach — fix the abstraction, not the 18 templates
Put .form on the root <form> and make the four dot-card-field components emit the canonical markup (.field, clean <label>, dotFieldRequired). All 18 field templates then inherit the fix without being touched individually.
dot-card-field is kept, not deleted — it provides two things the global system does not: the field-error-marker and :host ::ng-deep dot-card-field-footer:empty { display: none }.
Measured visual impact
html { font-size: 14px } (libs/dotcms-scss/angular/styles.scss:38, loaded via apps/dotcms-ui/project.json:75), and no typography scale override exists in libs/dotcms-scss/tailwind/theme.css. So 1rem = 14px:
| Property | Today | After | Delta |
|---|---|---|---|
| label font-size | 14px (inherited) | text-sm = 0.875rem = 12.25px |
−1.75px |
| label font-weight | 400 | font-medium = 500 |
+100 |
| gap label ↔ control | gap-2 = 7px |
gap-1 = 3.5px |
−3.5px |
| hint colour | default text colour | text-gray-500 |
fixed |
| error colour | default text colour | text-red-500 |
fixed (the bug) |
| content-type row/column layout | mb-5 gap-9 / gap-8 |
unchanged | 0 |
.form { space-y-5 } |
— | inert (<p-tabs> is the form's only direct child) |
0 |
| checkbox/radio rows | flex items-center gap-2 |
flex flex-row items-center gap-2 |
0 — identical |
| required asterisk | class written by hand | same class via the directive | 0 |
Nothing reflows: no field changes width, no column moves, no tab re-lays-out. The global values win — no local override is added to preserve today's look. Today the editor's labels are larger and lighter than every other admin form (dot-tags, locales, users, push publish all already render at 12.25px/500); that inconsistency is the reported problem.
Two implementation risks to resolve
1 — .form .field > label is a DIRECT-CHILD selector. If the <label> stays inside <dot-card-field-label>, that component's host element sits between .field and the label and the rule does not apply. This must be handled explicitly (e.g. display: contents on the dot-card-field-label host, or making the label a real direct child of .field). A backfill that adds .form and .field but leaves the label nested does not fix the reported problem.
2 — do NOT use checkIsRequiredControl mode. That mode reads Validators.required off the FormGroup, but required BLOCK_EDITOR fields in this editor use blockEditorRequiredValidator() instead (dot-edit-content-form.component.ts:766-778, getFieldValidators), so the asterisk would disappear from a required Block Editor. The source of truth for "required" here is the content type definition (field.required), exposed as the isRequired getter in libs/edit-content/src/lib/fields/shared/base-wrapper-field.ts:57-70. Use the directive's bare mode — <label dotFieldRequired> rendered conditionally on isRequired.
Acceptance Criteria
- The root
<form>indot-edit-content-form.component.htmlcarriesclass="form", and any hand-rolled layout class the global system already covers is removed from it. -
dot-card-fieldemits<div class="field">instead of<div class="flex flex-col gap-2">, and still renders thefield-error-markerand hides an emptydot-card-field-footer. -
.form .field > labelactually applies in the rendered DOM — the<label>is an effective direct child of.field(verify in the browser, not just in the template). - No
<label>insidelibs/edit-contentcarries aclassattribute with typography or spacing utilities. -
dot-card-field-labeluses thedotFieldRequireddirective in bare mode and no longer writesp-label-input-requiredby hand; same fordot-form-file-editor.component.html:47. - A required Block Editor field still shows its asterisk (the
blockEditorRequiredValidatorcase). - Checkbox and radio option rows use
.form-checkbox/.form-radioinstead of hand-rolledflex items-center gap-2. - Field hints render at
text-sm text-gray-500and validation errors attext-sm text-red-500in the running editor — they render unstyled today. -
language-fieldandsite-fieldlabels are clean, andsite-field'sforpoints at its own control id instead of"language-field". - The component SCSS that reimplements form layout in
dot-form-file-editoranddot-form-import-urlis removed in favour of the global system. - No local CSS or Tailwind utility is added anywhere in
libs/edit-contentto preserve the previous label typography or field gap. - Affected specs are updated and
pnpm nx test edit-contentandpnpm nx lint edit-contentpass. - The editor is exercised manually against a content type covering all field types (text, textarea, select, radio, checkbox, date/time, tags, block editor, wysiwyg, relationship, category, binary/file/image, key-value, json, custom field, host-folder, line divider) with no visual regression beyond the measured deltas above.
Priority
Medium
Additional Context
Scope — core-web/libs/edit-content only: the root form, the dot-card-field family, the field templates that need it, and the sub-components and dialogs inside that library (file editor, import URL, relationship search fields, sidebar workflow dialog). Out of scope: other portlets, the legacy JSP/Dojo editor, and the content type row/column grid (mb-5 grid gap-9 / flex flex-col gap-8) — that grid expresses the content type's own layout, which the global .form system cannot represent, and .form's rhythm never reaches it.
Reference implementation — core-web/libs/portlets/dot-tags/src/lib/dot-tags-create/dot-tags-create.component.html (zero component SCSS):
<form class="form" [formGroup]="form" (ngSubmit)="onSubmit()">
<div class="field">
<label for="tagName" dotFieldRequired>{{ 'tags.form.tag-name' | dm }}</label>
<input pInputText id="tagName" formControlName="name" class="w-full" />
</div>
</form>
Related
- Global stylesheet:
core-web/apps/dotcms-ui/src/style.css:35-95 - Standards:
docs/frontend/STYLING_STANDARDS.md(form system table + rules) - Directive:
core-web/libs/ui/src/lib/dot-field-required/dot-field-required.directive.ts - Automated review rule for new code:
dotCMS/dotcms-claude-plugins#33
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.