-
Notifications
You must be signed in to change notification settings - Fork 772
Refactor the lit renderer to separate the core renderer code from the catalog definition #484
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request is an excellent refactoring that separates the core rendering logic from component-specific definitions by introducing a Catalog system. This significantly improves modularity, maintainability, and extensibility, following the Open/Closed Principle. The removal of the large switch statement in A2uiMessageProcessor is a major cleanup. My feedback focuses on opportunities to further reduce boilerplate in the new catalog item definitions and simplify the API.
| constructor( | ||
| readonly opts: { | ||
| mapCtor: MapConstructor; | ||
| arrayCtor: ArrayConstructor; | ||
| setCtor: SetConstructor; | ||
| objCtor: ObjectConstructor; | ||
| } = { mapCtor: Map, arrayCtor: Array, setCtor: Set, objCtor: Object } | ||
| mapCtor?: MapConstructor; | ||
| arrayCtor?: ArrayConstructor; | ||
| setCtor?: SetConstructor; | ||
| objCtor?: ObjectConstructor; | ||
| catalog?: Catalog; | ||
| } = {} | ||
| ) { | ||
| this.arrayCtor = opts.arrayCtor; | ||
| this.mapCtor = opts.mapCtor; | ||
| this.setCtor = opts.setCtor; | ||
| this.objCtor = opts.objCtor; | ||
|
|
||
| this.surfaces = new opts.mapCtor(); | ||
| this.arrayCtor = opts.arrayCtor ?? Array; | ||
| this.mapCtor = opts.mapCtor ?? Map; | ||
| this.setCtor = opts.setCtor ?? Set; | ||
| this.objCtor = opts.objCtor ?? Object; | ||
| this.surfaces = new (this.mapCtor)(); | ||
| this.catalog = opts.catalog ?? new Catalog(StandardCatalogItems.items, 'a2ui://standard_catalog'); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The constructor options are now all optional, which is a good improvement for flexibility. However, the opts parameter itself is still marked as readonly, which is not necessary since it's a function parameter and not a class property. You can simplify the signature by removing the readonly keyword.
| constructor( | |
| readonly opts: { | |
| mapCtor: MapConstructor; | |
| arrayCtor: ArrayConstructor; | |
| setCtor: SetConstructor; | |
| objCtor: ObjectConstructor; | |
| } = { mapCtor: Map, arrayCtor: Array, setCtor: Set, objCtor: Object } | |
| mapCtor?: MapConstructor; | |
| arrayCtor?: ArrayConstructor; | |
| setCtor?: SetConstructor; | |
| objCtor?: ObjectConstructor; | |
| catalog?: Catalog; | |
| } = {} | |
| ) { | |
| this.arrayCtor = opts.arrayCtor; | |
| this.mapCtor = opts.mapCtor; | |
| this.setCtor = opts.setCtor; | |
| this.objCtor = opts.objCtor; | |
| this.surfaces = new opts.mapCtor(); | |
| this.arrayCtor = opts.arrayCtor ?? Array; | |
| this.mapCtor = opts.mapCtor ?? Map; | |
| this.setCtor = opts.setCtor ?? Set; | |
| this.objCtor = opts.objCtor ?? Object; | |
| this.surfaces = new (this.mapCtor)(); | |
| this.catalog = opts.catalog ?? new Catalog(StandardCatalogItems.items, 'a2ui://standard_catalog'); | |
| } | |
| constructor( | |
| opts: { | |
| mapCtor?: MapConstructor; | |
| arrayCtor?: ArrayConstructor; | |
| setCtor?: SetConstructor; | |
| objCtor?: ObjectConstructor; | |
| catalog?: Catalog; | |
| } = {} | |
| ) { | |
| this.arrayCtor = opts.arrayCtor ?? Array; | |
| this.mapCtor = opts.mapCtor ?? Map; | |
| this.setCtor = opts.setCtor ?? Set; | |
| this.objCtor = opts.objCtor ?? Object; | |
| this.surfaces = new (this.mapCtor)(); | |
| this.catalog = opts.catalog ?? new Catalog(StandardCatalogItems.items, 'a2ui://standard_catalog'); | |
| } |
| export class StandardCatalogItems { | ||
| /** | ||
| * An immutable list of all standard catalog items. | ||
| */ | ||
| static readonly items: readonly CatalogItem[] = Object.freeze([ | ||
| audioPlayerCatalogItem, | ||
| buttonCatalogItem, | ||
| cardCatalogItem, | ||
| checkBoxCatalogItem, | ||
| columnCatalogItem, | ||
| dateTimeInputCatalogItem, | ||
| dividerCatalogItem, | ||
| iconCatalogItem, | ||
| imageCatalogItem, | ||
| listCatalogItem, | ||
| modalCatalogItem, | ||
| multipleChoiceCatalogItem, | ||
| rowCatalogItem, | ||
| sliderCatalogItem, | ||
| tabsCatalogItem, | ||
| textFieldCatalogItem, | ||
| textCatalogItem, | ||
| videoCatalogItem, | ||
| ]); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To simplify the API, consider exporting a constant array directly instead of using a class with only a single static property. This avoids the need for consumers to access the .items property and makes the intent clearer.
This change would require updating the usage in model-processor.ts from StandardCatalogItems.items to standardCatalogItems.
export const standardCatalogItems: readonly CatalogItem[] = Object.freeze([
audioPlayerCatalogItem,
buttonCatalogItem,
cardCatalogItem,
checkBoxCatalogItem,
columnCatalogItem,
dateTimeInputCatalogItem,
dividerCatalogItem,
iconCatalogItem,
imageCatalogItem,
listCatalogItem,
modalCatalogItem,
multipleChoiceCatalogItem,
rowCatalogItem,
sliderCatalogItem,
tabsCatalogItem,
textFieldCatalogItem,
textCatalogItem,
videoCatalogItem,
]);| export const audioPlayerCatalogItem: CatalogItem = { | ||
| typeName: 'AudioPlayer', | ||
| buildNode( | ||
| baseNode: { id: string; dataContextPath: string; weight: number | 'initial' }, | ||
| resolvedProperties: ResolvedMap, | ||
| objCtor: ObjectConstructor | ||
| ): AnyComponentNode { | ||
| if (!isResolvedAudioPlayer(resolvedProperties)) { | ||
| throw new Error('Invalid properties for AudioPlayer component'); | ||
| } | ||
| return new objCtor({ | ||
| ...baseNode, | ||
| type: 'AudioPlayer', | ||
| properties: resolvedProperties, | ||
| }) as AnyComponentNode; | ||
| } | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a lot of repeated code across the new standard_catalog component files. To improve maintainability and reduce this boilerplate, you could introduce a factory function to generate these CatalogItem objects. This would centralize the common logic of property validation and node creation.
This comment applies to all similar new files in this directory (e.g., button.ts, card.ts, etc.).
A factory could look like this (perhaps in a shared utility file):
// e.g. in a new 'catalog_item_factory.ts'
import { CatalogItem } from '../catalog/catalog_item.js';
import { AnyComponentNode, ResolvedMap } from '../types/types.js';
export function createCatalogItem(
typeName: string,
guard: (props: unknown) => boolean
): CatalogItem {
return {
typeName,
buildNode(
baseNode: { id: string; dataContextPath: string; weight: number | 'initial' },
resolvedProperties: ResolvedMap,
objCtor: ObjectConstructor
): AnyComponentNode {
if (!guard(resolvedProperties)) {
throw new Error(`Invalid properties for ${typeName} component`);
}
return new objCtor({
...baseNode,
type: typeName,
properties: resolvedProperties,
}) as AnyComponentNode;
}
};
}Then, each component file would become much simpler:
// e.g. in audio_player.ts
import { createCatalogItem } from './catalog_item_factory.js'; // hypothetical path
import { isResolvedAudioPlayer } from '../data/guards.js';
export const audioPlayerCatalogItem = createCatalogItem('AudioPlayer', isResolvedAudioPlayer);
This PR refactors the Lit renderer to use a catalog-based architecture for component processing, making it more modular and extensible.
Key changes:
A detailed plan, including a proposal for future unification with the Angular renderer's catalog system, can be found in the newly added .