- name
- create-multi-describle-field-plugin
- description
- Create a MULTI_DESCRIBLE_PLUGIN field element plugin (multi-describable list element with table/list view, including valueChangePipe property linkage)
# Multi-Describle Field Plugin Creator
This skill helps you create a well-structured `MULTI_DESCRIBLE_PLUGIN` field element plugin that conforms to TIS plugin specifications. This type of plugin is used for list-based sub-forms where each row is a complete `Describable` instance.
## What is MULTI_DESCRIBLE_PLUGIN?
`MULTI_DESCRIBLE_PLUGIN` is a special field type in TIS that renders structured sub-form lists. Each element in the list is a full plugin instance with its own properties, validation, and lifecycle.
### Relationship Pattern
- **Host Plugin (1) ↔ Element Plugin (N)**: Many-to-one relationship
- **Example**: `UserProfile` (host) has `List<TestChild>` (elements)
- One `UserProfile` instance can have multiple `TestChild` instances
- Each `TestChild` is a complete plugin with its own properties and validation
### Key Characteristics
- **List View or Table View**: Frontend renders as `<nz-list>` or `<nz-table>`
- **Full Plugin Instances**: Each list item is a complete `Describable` object
- **Identity Requirement**: Each element must have a unique identity field
- **Enumerable or Open**: Can be a fixed candidate set (enumerable) or open for user creation
- **Linkable**: A host field change can replace the whole list, or rewrite the options of a field
inside every row (`valueChangePipe`, see [Property Linkage](#property-linkage-valuechangepipe))
## Core Requirements
### 1. Host Plugin Field Configuration
The host plugin (like `UserProfile`) must declare the field with specific attributes:
```java
// In UserProfile.java (Host Plugin)
@FormField(ordinal = 6,
desClazz = TestChild.class, // REQUIRED: Element class
type = FormFieldType.MULTI_DESCRIBLE_PLUGIN, // REQUIRED: Field type
validate = {Validator.require}) // OPTIONAL: Use if at least one item required
public List<TestChild> children;
```
**Key Points**:
- `desClazz`: Must specify the element class type (matches the List generic type)
- `type`: Must be `FormFieldType.MULTI_DESCRIBLE_PLUGIN`
- `validate = {Validator.require}`: Include if the list must have at least one element; omit if empty list is allowed
- Property type: Must be `List<ElementClass>` (generic List)
### 2. Element Plugin Class
The element class (like `TestChild`) must implement `IPluginStore.MultiDescribleElement`:
```java
// In TestChild.java (Element Plugin)
public class TestChild implements Describable<TestChild>,
IPluginStore.MultiDescribleElement {
// REQUIRED: Identity field
@FormField(identity = true, ordinal = 0, type = FormFieldType.INPUTTEXT,
validate = {Validator.require})
public String name;
// Other properties...
@FormField(ordinal = 1, type = FormFieldType.INT_NUMBER, validate = {Validator.require})
public Integer age;
// REQUIRED: Implement identity method
@Override
public String identityValue() {
return this.name;
}
}
```
**Critical Requirements**:
- **Must implement `IPluginStore.MultiDescribleElement` interface**
- Location: `tis-plugin/src/main/java/com/qlangtech/tis/plugin/IPluginStore.java:199`
- **Must have an identity field**:
- One `@FormField` property marked with `identity = true`
- Typically `String` type, often named `name` or `id`
- Must have `validate = {Validator.require}` (identity cannot be empty)
- **Must implement `identityValue()` method**:
- Returns the unique identifier for this element
- Used by TIS framework for element tracking and updates
### 3. Element Descriptor Class
The element's Descriptor must extend `IPluginStore.MultiDescribleElementDescriptor`:
```java
@TISExtension
public static class DftDescriptor extends IPluginStore.MultiDescribleElementDescriptor<TestChild> {
// REQUIRED: Display name (pure English, no Chinese/spaces)
@Override
public String getDisplayName() {
return "Test_Child"; // Underscore-separated PascalCase
}
// REQUIRED: Chinese description (implements DescriptorUseableShortComment)
@Override
public String shortComment() {
return "测试子表单属性"; // <=10 chars, no punctuation
}
// REQUIRED: End type (for UI rendering hints)
@Override
public EndType getEndType() {
return EndType.Copy;
}
// IMPORTANT: Override these methods based on your needs
@Override
public ViewStyle viewStyle() {
return ViewStyle.Table; // or ViewStyle.List
}
@Override
public List<ColConfig> colsConfig() {
// REQUIRED for Table view, at least one column must be clickable
return Lists.newArrayList(
new ColConfig("name", 12).setClickable(), // clickable column for editing
new ColConfig("age", 12),
new ColConfig("sex")
);
}
@Override
public EnumableSetCfg getEnumableSetCfg() {
// Return EnumableSetCfg(true) for fixed candidate set
// Return EnumableSetCfg(false) for open user creation (default)
return new EnumableSetCfg(false);
}
@Override
public int maxHeight() {
return 300; // px, default is 100
}
@Override
protected RowDragDropCfg isEnableRowDragDrop() {
// Enable row drag-drop for manual sorting
return new RowDragDropCfg(false, null); // default: disabled
}
@Override
protected List<Pair<Option, MultiDescribleElement>> getEnumableCandidateSet(
ParseDescribable<?> describable) {
// Override if getEnumableSetCfg() returns true
// Return the fixed candidate list
return Lists.newArrayList();
}
}
```
**Descriptor Location**: `tis-plugin/src/main/java/com/qlangtech/tis/plugin/IPluginStore.java:211`
### 4. Element Plugin Specifications
The element plugin MUST also satisfy standard TIS plugin requirements from `/create-plugin`:
- Proper `@FormField` annotations on all properties
- Corresponding `.json` descriptor file in resources
- Optional `.md` help file for complex properties
- Follow all validation rules, FormFieldType principles, and naming conventions
**Reference**: See `.claude/skills/create-plugin/SKILL.md` for complete plugin specifications
## Descriptor Methods Reference
### Required Methods
| Method | Purpose | Default | When to Override |
|--------|---------|---------|------------------|
| `getDisplayName()` | Plugin name (pure English) | Required | Always (English name) |
| `shortComment()` | Chinese description | Required | Always (<=10 chars) |
| `getEndType()` | UI rendering hint | Required | Always |
### View Configuration Methods
| Method | Purpose | Default | When to Override |
|--------|---------|---------|------------------|
| `viewStyle()` | List or Table view | `ViewStyle.List` | Override for Table view |
| `colsConfig()` | Table columns config | Empty list | Required for Table view (>=1 clickable) |
| `maxHeight()` | List/table max height (px) | 100 | Override for taller lists |
### Advanced Configuration Methods
| Method | Purpose | Default | When to Override |
|--------|---------|---------|------------------|
| `getEnumableSetCfg()` | Enumerable candidate set | `false` (open) | Override for fixed candidate pool |
| `getEnumableCandidateSet()` | Provide candidates | Empty list | Override when enumerable=true |
| `isEnableRowDragDrop()` | Manual row sorting | Disabled | Override to enable drag-drop |
| `createRelevantFieldKeysCreator()` | Field whitelist factory | Standard | Override for custom dependencies |
### Property Linkage (declared on the **host** descriptor)
| Method | Purpose | Notes |
|--------|---------|-------|
| `valueChangePipe(fromField, toField...)` | Link a host field change to a target | Call in the descriptor **constructor**; repeated calls with the same `fromField` merge |
| `.render(fn)` | Broadcast render | Called once per pipe, shared by all `toField` |
| `.render(toField, fn)` | Per-field render | Overrides broadcast; `toField` must already be registered |
**Detailed Documentation**: All methods have extensive Javadoc in `IPluginStore.java:211` (recently added by this conversation).
## View Styles
### List View vs Table View
| Feature | List View | Table View |
|---------|-----------|------------|
| Component | `<nz-list>` | `<nz-table>` |
| Density | Loose (more vertical space) | Compact (tight rows) |
| Column Config | Optional | Required (>=1 clickable) |
| Drag-drop | Not supported | Supported |
| Best for | <=3 fields, long text | >=4 fields, short values |
### Table View Column Configuration
```java
@Override
public List<ColConfig> colsConfig() {
return Lists.newArrayList(
new ColConfig("parameterId", 15).setClickable(), // 15% width, clickable
new ColConfig("displayName", 15), // 15% width
new ColConfig("dataType") // auto width
);
}
```
**Rules**:
- **At least one column must be clickable** - this opens the edit panel
- Column width is percentage (15 = 15% of table width)
- `null` width = auto-allocate remaining space
- Column order is independent of `@FormField(ordinal=...)`
## Enumerable Set Mode
### Open Set (Default) vs Enumerable Set
| Mode | User Behavior | Use Case |
|------|---------------|----------|
| **Open Set** (`false`) | Click "Add" -> Fill blank form -> Create freely | User defines arbitrary items |
| **Enumerable Set** (`true`) | Click "Add" -> Select from checkboxes -> Add selected | Choose from predefined templates |
### Implementing Enumerable Set
```java
@Override
public EnumableSetCfg getEnumableSetCfg() {
// itemDependencyDepth:
// 0 = depends on direct host instance (default)
// 1 = depends on host's parent instance
// 2+ = depends on ancestor at that depth
return new EnumableSetCfg(true, 0);
}
@Override
protected List<Pair<Option, MultiDescribleElement>> getEnumableCandidateSet(
ParseDescribable<?> describable) {
List<Pair<Option, MultiDescribleElement>> candidates = Lists.newArrayList();
// 1. Extract host instance from describable
UserProfile userProfile = describable.getInstance();
// 2. Get currently selected items (to mark them as checked)
Set<String> selected = userProfile.children.stream()
.map(TestChild::identityValue)
.collect(Collectors.toSet());
// 3. Generate candidate list from business logic
List<TestChild> templates = UserProfile.getChildren(userProfile.control);
// 4. Convert to Option/Element pairs
for (TestChild template : templates) {
Option opt = new Option(template.identityValue())
.setChecked(selected.contains(template.identityValue()));
candidates.add(Pair.of(opt, template));
}
return candidates;
}
```
**Key Points**:
- `describable.getInstance()` gives you a **partially-instantiated** host object
- Only fields in the relevant fields whitelist have values
- Other fields are `null`
- `Option.value` must match `MultiDescribleElement.identityValue()`
- Use `.setChecked(true)` for already-selected items
**Not the same as property linkage.** `getEnumableCandidateSet` is re-invoked every time the 「设置」 dialog opens, so it *does* see the host's current values — but it does nothing when a host field changes while the dialog is closed. If the list (or a per-row dropdown) must react to a host field change **in place**, you need `valueChangePipe` (next section), not this hook.
## Property Linkage (`valueChangePipe`)
A field on the host changes → something inside the child list must follow. Three shapes involve a
`MULTI_DESCRIBLE_PLUGIN` field, and these are the three this skill covers (2b is 2 one level deeper):
| # | Shape | `toField` form | What changes |
|---|---|---|---|
| 1 | **Child list replacement** | `"<listField>"` | The whole `List<T>` is swapped for a new batch of records |
| 2 | **Per-row child property options** | `"<listField>[].<childField>"` — e.g. `"children[].reactSelect"` | The candidate options of `<childField>` inside **every existing row** |
| 2b | **Per-row grandchild options** (through a nested polymorphic sub-form) | `"<listField>[].<descField>.<leafField>"` — e.g. `"series[].aggregation.property"` | The candidate options of `<leafField>` inside `<descField>`'s **every impl**, for every existing row |
Note the `.` in shapes 2/2b: the frontend splits `toField` on dots and supports **at most three
segments** (`item-prop-val.component.ts#propValChange`); anything longer is `console.warn` + skip.
Segments 2b adds: `<descField>` is itself a `@FormField`-declared polymorphic sub-form (an
`AttrDesc` with `describable=true` and several `descriptors`), so the pipe must rewrite the
`<leafField>` AttrDesc **in each impl's Descriptor** — one AttrDesc is not enough. A three-segment
`toField` whose first segment is *not* a `MULTI_DESCRIBLE_PLUGIN` field is rejected too: there is no
row instance to backfill and the linkage would silently do nothing.
Both are declared in the **descriptor constructor**. There is no annotation for this:
```java
protected ValueChangePipe valueChangePipe(String fromField, String... toField)
```
### Declaring it
```java
@TISExtension
public static final class DftDescriptor extends ParamsConfig.BasicParamsConfigDescriptor {
public DftDescriptor() {
View on GitHub