Skip to content

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

Description

@adrianjm-dotCMS

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions