Skip to content

Conversation

@jacobsimionato
Copy link
Collaborator

@jacobsimionato jacobsimionato commented Jan 13, 2026

This PR refactors the Lit renderer to use a catalog-based architecture for component processing, making it more modular and extensible.

Key changes:

  • Introduces a class and interface to define how components are processed from server messages.
  • Moves all standard component implementation logic out of the core and into a new class. Each component's logic is now in its own file under .
  • Refactors to use the provided catalog, making it possible for developers to provide their own custom component catalogs.
  • This is a non-breaking change for the Angular renderer, which continues to compile and work as expected.

A detailed plan, including a proposal for future unification with the Angular renderer's catalog system, can be found in the newly added .

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a 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.

Comment on lines 57 to 72
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');
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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');
}

Comment on lines +40 to +64
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,
]);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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,
]);

Comment on lines +21 to +37
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;
}
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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);

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant