dotCMS / dotCMS/core

Backfill: adopt the global .form / .field form system in the new Angular Edit Content editor

Open
#37,460 1 comment 0 reactions 1 assignee View on GitHub

@adrianjm-dotCMS is already working on this.

Since Sep 16, 2026.

dotCMS : Technical Debt dotCMS: New Edit Contentlet Team : Falcon Type : Task
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> in dot-edit-content-form.component.html carries class="form", and any hand-rolled layout class the global system already covers is removed from it.
  • dot-card-field emits <div class="field"> instead of <div class="flex flex-col gap-2">, and still renders the field-error-marker and hides an empty dot-card-field-footer.
  • .form .field > label actually 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> inside libs/edit-content carries a class attribute with typography or spacing utilities.
  • dot-card-field-label uses the dotFieldRequired directive in bare mode and no longer writes p-label-input-required by hand; same for dot-form-file-editor.component.html:47.
  • A required Block Editor field still shows its asterisk (the blockEditorRequiredValidator case).
  • Checkbox and radio option rows use .form-checkbox / .form-radio instead of hand-rolled flex items-center gap-2.
  • Field hints render at text-sm text-gray-500 and validation errors at text-sm text-red-500 in the running editor — they render unstyled today.
  • language-field and site-field labels are clean, and site-field's for points at its own control id instead of "language-field".
  • The component SCSS that reimplements form layout in dot-form-file-editor and dot-form-import-url is removed in favour of the global system.
  • No local CSS or Tailwind utility is added anywhere in libs/edit-content to preserve the previous label typography or field gap.
  • Affected specs are updated and pnpm nx test edit-content and pnpm nx lint edit-content pass.
  • 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

Scopecore-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 implementationcore-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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.