react-component-patterns
Conventions for writing React components in this codebase.
- License 许可
- MIT
- 最近更新
- 2026-11-09
- Stars
- ★ 344
- Forks
- 7
01 前言 Front Matter
- name
- scaffold-form-fragment
- description
- Create a Liferay input fragment that lives inside a Form Container and binds to an object field. Use when the user asks to build a form field, add a custom input to a form, create a picker or stepper for a form, or make a fragment that submits to an object.
02 资源文件 4
- 参考文档automations.md1000 B
- 参考文档hooks.json523 B
- 参考文档INDEX.md4.8 KB
- 参考文档subagent.md545 B
03 SKILL.md 正文
Scaffold Form Fragment
Build a fragment of type input — a form field an author drops into a Form Container, which binds it to a field on the container's object.
When to Invoke
- "Build a form field", "add a custom input to a form", "make a date picker for this form"
- "Create a fragment that submits to an object"
- Called by
scaffold-fragmentwhen the fragment being built is a form field rather than a display component
For a display fragment — hero, card, section — use scaffold-fragment instead. Everything about collections, naming, deploy, and reprovision lives there and is not repeated here.
Prerequisites
- A published object with the target field. Load
manage-objects; an unpublished definition exposes no fields to bind against. - A page carrying a Form Container bound to that object. Load
manage-pages.
No feature flag gates input fragments.
Workflow
Declare the Fragment as an Input
fragment.json needs two things beyond a normal fragment: "type": "input", and a typeOptions block naming the field types the fragment can bind to.
{
"cacheable": false,
"fragmentEntryKey": "brand-email-input",
"icon": "envelope-closed",
"name": "Brand Email Input",
"type": "input",
"typeOptions": {
"fieldTypes": [
"email"
]
}
}
fieldTypes is nested inside typeOptions — not a top level key. typeOptions is schema validated with additionalProperties: false, so fieldTypes is the only key it accepts.
Declare only the types the fragment actually renders. The Form Container offers the fragment for a field whose type appears in this list, so an overly broad list surfaces the fragment where it cannot work.
| Field Type | Object Field It Binds To |
|---|---|
boolean |
Boolean |
captcha |
None — renders a CAPTCHA, not a field |
date |
Date |
date-time |
Date and Time |
email |
Text constrained to email |
file |
Attachment |
formButton |
None — renders the submit control |
friendly-url |
Text constrained to a friendly URL |
html |
Rich Text |
long-text |
Long Text |
multiselect |
Picklist, multiple values |
number |
Integer, Decimal, Precision Decimal |
phone-number |
Text constrained to a phone number |
relationship |
Relationship |
select |
Picklist, single value |
stepper |
None — renders form step navigation |
text |
Text |
Set "cacheable": false. Liferay refuses to cache an input fragment regardless, and stating it keeps the file honest.
Source: FragmentEntryValidatorImpl and the built in input fragments under modules/apps/fragment/fragment-collection-contributor/fragment-collection-contributor-inputs.
Render the Field
The container injects an input object into the template. Read the field's label, value, and state from it — never hardcode them, and never invent a name.
| Property | Type | Holds |
|---|---|---|
input.attributes |
map | Type specific metadata, plus readOnly. Contents vary by field type |
input.errorMessage |
string | Server side validation message; empty when valid |
input.helpText |
string | Author supplied help text |
input.label |
string | The field label |
input.localizable |
boolean | Whether the field stores a value per locale |
input.name |
string | The name attribute the container reads on submit |
input.readOnly |
boolean | Whether the field rejects edits |
input.required |
boolean | Whether the field is mandatory |
input.showHelpText |
boolean | Whether the author chose to show help text |
input.showLabel |
boolean | Whether the author chose to show the label |
input.type |
string | The bound field's type |
input.value |
string | Current value, populated when editing an existing entry |
input.valueI18n |
map | Per locale values, keyed by language ID |
${input.name} is the whole binding. The container maps the submitted value to the object field by that name, so a hardcoded or misspelled name submits nothing and reports no error.
Namespace every id with ${fragmentElementId} so the fragment survives being placed twice on one page.
<div class="form-group [#if input.errorMessage?has_content]has-error[/#if] mb-0" id="${fragmentElementId}-form-group">
<label class="[#if !input.showLabel || !input.label?has_content]sr-only[/#if]" for="${fragmentElementId}-input" id="${fragmentElementId}-input-label">${htmlUtil.escape(input.label)}
[#if input.required][@clay["icon"] className="reference-mark" symbol="asterisk" /][/#if]</label>
<input class="form-control" id="${fragmentElementId}-input" name="${input.name}" [#if input.readOnly]readonly[/#if] ${input.required?then('required', '')} type="email" [#if input.value??]value="${input.value}"[/#if] />
[#if input.showHelpText && input.helpText?has_content]
<p class="mb-0 mt-1 text-secondary" id="${fragmentElementId}-input-help-text">${htmlUtil.escape(input.helpText)}</p>
[/#if]
</div>
Escape input.label and input.helpText with htmlUtil.escape in the template. The JSON handed to index.js is already escaped; the FreeMarker object is not.
Use Lexicon classes (form-group, form-control, has-error, sr-only) and the Clay icon macro so the field matches every other field in the container.
Wire the Value
Three things belong in index.js.
Disable the control in edit mode. layoutMode is 'edit' while an author composes the page. Leaving the field live lets an author type into a form they are only arranging.
const inputElement = document.getElementById(`${fragmentElementId}-input`);
if (layoutMode === 'edit') {
inputElement.setAttribute('disabled', true);
}
Register the field so error rendering and localization behave like the built in inputs. @liferay/fragment-impl/api exposes registerInputFeedback for error display, and registerLocalizedInput / registerUnlocalizedInput for value handling. Branch on input.localizable — a localizable field needs the per locale inputs the helper creates, and skipping it silently drops every locale but the default.
Sync custom UI to a hidden input. A slider, a map picker, or a multi select built from divs submits nothing on its own. Carry a hidden input named ${input.name}, write to it, and dispatch a bubbling change so the container notices.
customControl.addEventListener('change', (event) => {
hiddenInput.value = event.target.value;
hiddenInput.dispatchEvent(new Event('change', {bubbles: true}));
});
Without the dispatched event the value sits in the DOM and the container reads the previous one.
Deploy and Place
Deploy exactly as a display fragment — the fragment lives in the site initializer tree and appears when the site is provisioned. See scaffold-fragment for the file layout and rules/site-initializer-format.md for the reprovision recipe.
Then bind it: open the page, select the Form Container's field, and choose the fragment from the field's input list. It appears only for fields whose type is in typeOptions.fieldTypes.
Patterns and Gotchas
Four fieldTypes mistakes throw and abort the fragment import — the collection fails to load and the fragment never appears:
| Condition | Message |
|---|---|
captcha listed alongside any other field type |
Captcha field type cannot be mixed with other field types |
fieldTypes present on any type other than input |
Only fragment type input can have field types |
stepper listed alongside any other field type |
Stepper field type cannot be mixed with other field types |
type: "input" with an empty or absent fieldTypes |
Fragment type input must have at least one field type |
input exists only for type: "input". The renderer injects it behind an isTypeInput() check, so referencing input from a component or section fragment throws a reference error at render. configuration, fragmentElement, fragmentElementId, and fragmentEntryLinkNamespace are injected for every fragment type.
Use fragmentElementId, not fragmentEntryLinkNamespace, for element ids. Both are injected, and the built in input fragments use fragmentElementId throughout — matching them keeps ids consistent with the helpers in @liferay/fragment-impl/api, which take it as their namespace.
A hidden field still submits. CSS display: none removes a conditionally shown field from view and leaves it in the form, so it posts a value the visitor never entered. Remove it from the DOM in index.js instead. Conditional visibility built with <lfr-drop-zone> has the same trap — see scaffold-fragment → "Drop Zones".
Public forms are write only. A form open to anonymous visitors needs ADD_OBJECT_ENTRY and not VIEW on the object; company scope VIEW publishes every submission to the world. The visitor's own POST still returns the created entry, so the fragment can confirm back what was recorded. Read rules/guest-access.md before exposing a form on a public page.
Edit mode styling. An input fragment with scroll animations or collapsed sections needs the has-edit-mode-menu treatment so an author can reach the field. See scaffold-fragment → "Edit Mode Awareness".
Success Signal
The fragment appears in the input list for a field of a declared type, and a submission through it lands as an object entry:
curl \
--silent \
--url "http://localhost:${PORT}/o/c/<pluralLabel>" \
--user "test@liferay.com:test" \
| jq '.items[-1]'
The submitted value appears on the field named by input.name. A 200 with the field empty or absent means the name attribute did not reach the container — check for a hardcoded name or a missing dispatched change.
04 同仓库更多 Skills
promptscript
PromptScript language expert for reading, writing, modifying, and troubleshooting .prs files. Use when working with PromptScript syntax, creating or editing .prs files, adding blocks like @identity, @standards, @restrictions, @shortcuts, @skills, or @agents, configuring promptscript.yaml, resolving compilation errors, understanding inheritance (@inherit), composition (@use, @extend, @override), contextual @header metadata, or migrating AI instructions to PromptScript. Also use when asked about the 50 built-in coding agent compilation targets, including GitHub Copilot, Claude Code, Cursor, Antigravity, Factory AI, and AGENTS.md-based platforms, or about the bundle and prompt-text targets for application prompts.
expert
Base expert skill
tdd-workflow
Test-driven development workflow
alpha
Alpha skill
pdf-processing
Extract PDF text, fill forms, merge files. Use when handling PDFs.
deploy-app
Deploy the application to staging or production environments. Use when deploying code or when the user mentions deployment, releases, or environments.