skills/pimcore-studio-ui-dynamic-types/SKILL.md
Dynamic type system fundamentals in Pimcore Studio - extensible type pattern for polymorphic behavior
npx skillsauth add pimcore/skills pimcore-studio-ui-dynamic-typesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
The fundamental concept of Dynamic Types in Pimcore Studio:
Note: For specific dynamic type implementations, see:
grid-cells.md - Grid cell type examplesfield-definitions.md - Data object field definition typesdata-types.md - Data object data typesUse this when:
BEFORE writing any import, read CRITICAL-IMPORT-PATHS.md.
All examples below use bundle imports (@pimcore/studio-ui-bundle/*). For core development (@sdk/*, @Pimcore/*), see the referenced file.
Dynamic Types are an architectural pattern for creating extensible, polymorphic type systems in Pimcore Studio.
In complex UIs, you often have many types of things that need different behavior:
Traditional approach problems:
if type === 'text' ... else if type === 'number')Dynamic Types provide a plugin architecture for types:
┌─────────────────────────────────────────┐
│ DynamicTypeAbstract (base class) │
│ - id: string │
│ - getComponent(): ReactElement │
└─────────────────────────────────────────┘
▲
│ extends
│
┌─────────┴──────────┬──────────┐
│ │ │
┌───┴────┐ ┌──────┴───┐ ┌──┴─────┐
│TextType│ │NumberType│ │DateType│
│id='text'│ │id='number'│ │id='date'│
└────────┘ └──────────┘ └────────┘
│ │ │
└──────────────────┴────────────┘
│
┌─────────▼──────────┐
│ TypeRegistry │
│ .get('text') │
│ .get('number') │
│ .get('date') │
└────────────────────┘
Defines the interface all types must implement:
// Core defines the base
export abstract class DynamicTypeAbstract {
abstract id: string
abstract getComponent(props: any): ReactElement
}
Each type implements the interface:
import { injectable } from '@pimcore/studio-ui-bundle/app'
import { DynamicTypeAbstract } from './base'
@injectable()
export class TextType extends DynamicTypeAbstract {
id = 'text'
getComponent(props) {
return <TextComponent {...props} />
}
}
Key elements:
@injectable() - Registers as DI serviceid - Unique identifier for type lookupManages all registered types:
export class DynamicTypeRegistry {
private types = new Map<string, DynamicTypeAbstract>()
registerDynamicType(type: DynamicTypeAbstract): void {
this.types.set(type.id, type)
}
getDynamicType(id: string): DynamicTypeAbstract | undefined {
return this.types.get(id)
}
}
UI components look up types at runtime:
// Component needs to render based on type
const SomeComponent = ({ typeId, ...props }) => {
const registry = useInjection<TypeRegistry>(serviceIds.typeRegistry)
const type = registry.getDynamicType(typeId)
if (!type) return <DefaultComponent {...props} />
// Render type-specific component
return type.getComponent(props)
}
Pimcore Studio uses dynamic types in multiple contexts:
Custom cell renderers for data grids.
Base: DynamicTypeGridCellAbstract
Registry: DynamicTypeGridCellRegistry
Use: Custom data visualization in grids
See: grid-cells.md
Data object field definitions (input, textarea, numeric, etc.).
Base: DynamicTypeFieldDefinitionAbstract
Registry: DynamicTypeFieldDefinitionRegistry
Use: Field configuration forms
See: field-definitions.md
Data type implementations for data objects.
Base: DynamicTypeDataObjectAbstract
Registry: DynamicTypeDataObjectRegistry
Use: Field rendering and editing in data object editors
See: data-types.md
Custom layout components for editors.
Base: DynamicTypeLayoutAbstract
Registry: DynamicTypeLayoutRegistry
Use: Specialized editor layouts
Custom metadata field types.
Base: DynamicTypeMetadataAbstract
Registry: DynamicTypeMetadataRegistry
Use: Asset/document metadata fields
Custom filter types for data filtering.
Base: DynamicTypeFilterAbstract
Registry: DynamicTypeFilterRegistry
Use: Complex filter UIs
BEFORE writing any import, read CRITICAL-IMPORT-PATHS.md.
[module-name]/
├── dynamic-types/
│ ├── definitions/ # Type definition classes (OR use types/)
│ │ ├── [type-name]/ # Optional: subfolder for complex types
│ │ │ ├── dynamic-type-[category]-[type-name].tsx
│ │ │ └── [component-files].tsx
│ │ └── dynamic-type-[category]-abstract.tsx # Abstract base
│ ├── components/ # Shared React components (optional)
│ │ └── [component-name]/
│ │ ├── [component-name].tsx
│ │ └── [component-name].styles.tsx
│ ├── registry/ # Registry class
│ │ └── dynamic-type-[category]-registry.ts
│ ├── hooks/ # Custom React hooks (optional)
│ └── index.ts # Module registration
Pattern: dynamic-type-[category]-[type-name].tsx
Examples:
dynamic-type-grid-cell-status-badge.tsxdynamic-type-field-definition-select.tsxdynamic-type-theme-studio-default-light.tsdynamic-type-filter-text.tsxPattern: dynamic-type-[category]-abstract.tsx
Examples:
dynamic-type-grid-cell-abstract.tsxdynamic-type-field-definition-abstract.tsxdynamic-type-theme-abstract.tsPattern: dynamic-type-[category]-registry.ts
Examples:
dynamic-type-grid-cell-registry.tsdynamic-type-field-definition-registry.tsxdynamic-type-theme-registry.tsPattern: [component-name]-[purpose].tsx or [component-name].tsx
Examples:
status-badge-cell.tsx (grid cell component)field-definition-select-form-fields.tsx (companion form)asset-preview-cell.tsxPattern: [component-name].styles.tsx
Examples:
status-badge-cell.styles.tsxcheckbox-cell.styles.tsxmy-bundle/
└── assets/studio/js/src/
└── modules/
└── custom-grid-cells/
├── dynamic-types/
│ ├── definitions/
│ │ ├── status-badge/
│ │ │ └── dynamic-type-grid-cell-status-badge.tsx
│ │ └── progress-bar/
│ │ └── dynamic-type-grid-cell-progress-bar.tsx
│ ├── components/
│ │ ├── status-badge/
│ │ │ ├── status-badge-cell.tsx
│ │ │ └── status-badge-cell.styles.tsx
│ │ └── progress-bar/
│ │ └── progress-bar-cell.tsx
│ └── index.ts
└── modules/
└── grid-cell-extension.tsx
dynamic-types/
└── definitions/
└── dynamic-type-grid-cell-text.tsx
dynamic-types/
├── definitions/
│ └── select/
│ ├── dynamic-type-field-definition-select.tsx
│ └── field-definition-select-form-fields.tsx
└── components/
└── select-options-grid/
└── select-options-grid.tsx
dynamic-types/
├── definitions/
│ ├── type-a/
│ │ └── dynamic-type-grid-cell-type-a.tsx
│ └── type-b/
│ └── dynamic-type-grid-cell-type-b.tsx
└── components/ # Shared between types
└── common-formatter/
└── common-formatter.tsx
types/ Instead of definitions/Some modules use types/ instead of definitions/:
dynamic-types/
├── types/ # Instead of definitions/
│ ├── select/
│ │ └── dynamic-type-field-definition-select.tsx
│ └── image/
│ └── dynamic-type-field-definition-image.tsx
└── registry/
└── dynamic-type-field-definition-registry.tsx
Note: Use EITHER definitions/ OR types/, not both.
dynamic-types/
└── types/
└── _abstracts/ # Private/internal abstracts
├── data/
│ └── dynamic-type-field-definition-data-abstract.tsx
└── layout/
└── dynamic-type-field-definition-layout-abstract.tsx
dynamic-types/
├── definitions/
├── components/
├── hooks/ # Custom React hooks
│ └── use-type-options.ts
└── utils/ # Utility functions
└── format-helpers.ts
File: dynamic-types/index.ts or modules/[feature]-extension.tsx
// dynamic-types/index.ts
import { type AbstractModule, container } from '@pimcore/studio-ui-bundle'
import { serviceIds } from '@pimcore/studio-ui-bundle/app'
export const DynamicTypeExtension: AbstractModule = {
onInit: () => {
const registry = container.get<DynamicTypeGridCellRegistry>(
serviceIds['DynamicTypes/GridCellRegistry']
)
// Register each type
registry.registerDynamicType(
container.get('DynamicTypes/GridCell/StatusBadge')
)
registry.registerDynamicType(
container.get('DynamicTypes/GridCell/ProgressBar')
)
}
}
dynamic-type-[category]-[name] patterncomponents/ for reusable piecesindex.ts for easier importsdynamic-types/
├── definitions/
│ └── [cell-type]/
│ └── dynamic-type-grid-cell-[cell-type].tsx
└── components/
└── [cell-type]/
├── [cell-type]-cell.tsx
└── [cell-type]-cell.styles.tsx
dynamic-types/
├── types/
│ └── [field-type]/
│ ├── dynamic-type-field-definition-[field-type].tsx
│ └── field-definition-[field-type]-form-fields.tsx
└── components/ # Shared across field types
dynamic-types/
└── definitions/
├── dynamic-type-theme-abstract.ts
├── [theme-name]/
│ └── dynamic-type-theme-[theme-name].ts
└── registry/
└── dynamic-type-theme-registry.ts
import { injectable } from '@pimcore/studio-ui-bundle/app'
import { DynamicTypeGridCellAbstract } from '@pimcore/studio-ui-bundle/modules/element'
import type { AbstractGridCellDefinition } from '@pimcore/studio-ui-bundle/modules/element'
import React, { type ReactElement } from 'react'
@injectable()
export class MyCustomType extends DynamicTypeGridCellAbstract {
// Unique identifier
id = 'my-custom-type'
// Return the component for this type
getGridCellComponent(props: AbstractGridCellDefinition): ReactElement {
return <MyCustomComponent {...props} />
}
}
Requirements:
@injectable() decoratorid propertyimport { type AbstractGridCellDefinition } from '@pimcore/studio-ui-bundle/modules/element'
import { Badge } from '@pimcore/studio-ui-bundle/components'
import React from 'react'
export const MyCustomComponent = ({
getValue,
row,
column
}: AbstractGridCellDefinition): React.JSX.Element => {
const value = getValue()
return (
<div className="default-cell__content">
<Badge color="blue">{value}</Badge>
</div>
)
}
Update your bundle's service configuration:
// In your plugin or module setup
import { container } from '@pimcore/studio-ui-bundle'
import { MyCustomType } from './dynamic-types/my-custom-type'
// Register as DI service with unique ID
container.bind('DynamicTypes/GridCell/MyCustomType')
.to(MyCustomType)
.inSingletonScope()
import { type AbstractModule, container } from '@pimcore/studio-ui-bundle'
import { serviceIds } from '@pimcore/studio-ui-bundle/app'
import type { DynamicTypeGridCellRegistry } from '@pimcore/studio-ui-bundle/modules/element'
export const MyModule: AbstractModule = {
onInit: () => {
// Get the registry
const registry = container.get<DynamicTypeGridCellRegistry>(
serviceIds['DynamicTypes/GridCellRegistry']
)
// Register your type (get from DI container)
registry.registerDynamicType(
container.get('DynamicTypes/GridCell/MyCustomType')
)
}
}
Multiple types need different behavior
Types need to be extensible
Type behavior is reusable
Runtime type resolution
Simple conditional rendering
Single-use components
Fixed, unchanging types
Bundles add types without core changes:
// Core provides: text, number, date
// Bundle adds: currency, percentage, status
// All registered in same registry
// All work seamlessly together
Same type renders consistently everywhere:
// Type registered once
registry.registerDynamicType(StatusBadgeType)
// Used automatically across:
// - Data object grids
// - Asset listings
// - Custom tables
// - Search results
TypeScript enforces correct implementation:
// Must extend base class
class MyType extends DynamicTypeAbstract {
// Must implement required properties/methods
id: string = 'my-type'
getComponent(props: Props): ReactElement {
// TypeScript checks return type
}
}
Type logic separated from UI:
// UI doesn't know about specific types
const Cell = ({ typeId, ...props }) => {
const type = registry.getDynamicType(typeId)
return type.getComponent(props)
}
// Types added independently
registry.registerDynamicType(newType)
Types can be tested in isolation:
describe('MyCustomType', () => {
it('renders correctly', () => {
const type = new MyCustomType()
const component = type.getComponent({ value: 'test' })
// Test component
})
})
Types often need configuration:
@injectable()
export class ConfigurableType extends DynamicTypeAbstract {
id = 'configurable'
getComponent(props: Props & { options?: Options }): ReactElement {
const { options = defaultOptions } = props
return <Component {...props} options={options} />
}
}
// Usage with options
const typeInstance = registry.getDynamicType('configurable')
return typeInstance.getComponent({
value: 'data',
options: { format: 'detailed', color: 'blue' }
})
Access additional context in types:
@injectable()
export class ContextAwareType extends DynamicTypeAbstract {
id = 'context-aware'
getComponent(props: Props): ReactElement {
return <ContextAwareComponent {...props} />
}
}
const ContextAwareComponent = ({ value, context }) => {
// Access context data
const { userId, permissions } = context
// Conditional rendering based on context
if (permissions.includes('admin')) {
return <AdminView value={value} />
}
return <UserView value={value} />
}
bundle:type-name) to avoid conflicts@injectable() decorator❌ Forgetting @injectable()
// BAD - won't be available in DI container
export class MyType extends DynamicTypeAbstract {
✅ Always decorate
// GOOD
@injectable()
export class MyType extends DynamicTypeAbstract {
❌ Registering manually created instances
// BAD - bypasses DI container
registry.registerDynamicType(new MyType())
✅ Get from container
// GOOD - uses DI container
registry.registerDynamicType(
container.get('DynamicTypes/MyType')
)
❌ Non-unique IDs
// BAD - conflicts with other bundles
id = 'text'
✅ Namespace your IDs
// GOOD
id = 'my-bundle:custom-text'
/examples/dynamic-types/ in studio-example-bundle@pimcore/studio-ui-bundle/modules/elementserviceIds['DynamicTypes/*']tools
UX and UI design conventions for Pimcore Studio - layout, spacing, action labels, writing style, and design principles for consistent extensions
tools
Widget system in Pimcore Studio UI - registering widgets, opening them in layout areas, WidgetManagerTabConfig, and connecting widgets to navigation
tools
How bundles consume the Pimcore Studio UI SDK - plugins, modules, DI, registries, and imports
development
TypeScript coding standards and best practices for Pimcore Studio UI - type safety, null checks, and code quality