import { ContextScope } from '../context'; /** * Registry for view factories * * A view factory is used to create or transform a view representation from data, * typically a {@link LimeObject} but can also be any other data type. View factories * enable components to customize how data is displayed without modifying the core * rendering logic. * * Multiple factories can be registered for the same view type. They run in the * order they were registered, and each one receives the view returned by the * previous factory. The first receives `undefined`, because nothing has built a * view yet. * * A factory does not own the view it receives. It returns a new object instead * of changing the one it was given. See {@link ViewFactory} for the full rule. * * {@link ViewFactoryRegistry.registerFactory | registerFactory} and * {@link ViewFactoryRegistry.getFactory | getFactory} take their types from * the view type name, so neither call names a type argument. A name and its * factory type become known through a module augmentation, written by the * package that introduces the view type. * * A package that introduces a view type of its own writes the whole * augmentation itself, next to the component that renders the view. * TypeScript merges every such block, so the platform's view types and your * own are both known at the call site. Code that registers a factory for a * view type that already exists writes none of this. * * A name that no augmentation declares still compiles, because these blocks * add to the declarations below rather than replacing them. Its factory falls * back to `unknown` for both data and view, which is the signal that the * augmentation is missing. * * @example Chaining multiple factories * ```ts * import { PlatformServiceName } from '@limetech/lime-web-components'; * * const registry = platform.get(PlatformServiceName.ViewFactoryRegistry); * * registry.registerFactory('list', (limeobject, scope, view) => { * return { * ...view, * id: limeobject.id, * title: limeobject.getValue('name'), * icon: 'file', * }; * }); * * registry.registerFactory('list', (limeobject, scope, view) => { * if (!view) { * return view; * } * * return { * ...view, * subtitle: limeobject.getLimetype().localname.singular, * }; * }); * * registry.registerFactory('list', (limeobject, scope, view) => { * const status = limeobject.getValue('status'); * if (!view || !status) { * return view; * } * * return { * ...view, * badge: String(status).toUpperCase(), * }; * }); * ``` * * @example One time setup, introducing a view type of your own * ```ts * import { * PlatformServiceName, * ViewFactory, * } from '@limetech/lime-web-components'; * * // Written once, by the package that introduces the view type. TypeScript * // merges these blocks, so each package declares only its own view types. * * interface TeamMember { * username: string; * email: string; * } * * interface MemberCard { * title: string; * subtitle: string; * } * * type ViewFactories = { * 'user-card': ViewFactory; * }; * * // Narrowing both methods to the keys of the map is what removes the type * // argument from the call site. Registering a factory that does not match the * // view type it is registered under becomes a compile error. * declare module '@limetech/lime-web-components' { * interface ViewFactoryRegistry { * registerFactory>( * type: Key, * factory: ViewFactories[Key] * ): void; * * getFactory>( * type: Key * ): ViewFactories[Key] | undefined; * } * } * * const registry = platform.get(PlatformServiceName.ViewFactoryRegistry); * * // `member` and `view` are inferred from the 'user-card' entry. * registry.registerFactory('user-card', (member, scope, view) => { * return { ...view, title: member.username, subtitle: member.email }; * }); * ``` * * @beta * @group Views */ export interface ViewFactoryRegistry { /** * Get a factory function for the given view type * * With a single factory registered, that factory is returned as it is. With * several, the returned factory calls each one in turn and passes the view * from one to the next. If nothing is registered for the type, returns * `undefined`. * * @param type - the type of view to get a factory for (e.g., 'list', 'card', 'table') * @returns a composite factory function, or undefined if no factories are registered * * @example Getting a factory for a view type * ```ts * import { LimeObject, PlatformServiceName } from '@limetech/lime-web-components'; * * const registry = platform.get(PlatformServiceName.ViewFactoryRegistry); * * // The 'list' key fixes the return type, so `factory` is already known to * // build a ListItem from a LimeObject. * const factory = registry.getFactory('list'); * * const scope = platform.get(PlatformServiceName.ContextRegistry).scope({ * host: document.createElement('my-list'), * }); * * // `getFactory` returns undefined when nothing is registered for the type, * // so the call has to be guarded. * const toListItem = (limeobject: LimeObject) => factory?.(limeobject, scope); * ``` */ getFactory(type: string): ViewFactory | undefined; /** * Register a factory for a given view type * * Adds a factory to the chain for the view type. A factory registered later * runs after the ones already registered. * * A factory has to handle a `view` of `undefined`, which is what the first * factory in a chain receives. * * A factory must not change the `view` it receives. See * {@link ViewFactory} for what it may do with it instead. * * @param type - the type of view to register a factory for (e.g., 'list', 'card', 'table') * @param factory - factory function to create or transform a view of the specific type * * @example Register a single factory * ```ts * import { PlatformServiceName } from '@limetech/lime-web-components'; * * const registry = platform.get(PlatformServiceName.ViewFactoryRegistry); * * registry.registerFactory('list', (data, scope, view) => { * return { * ...view, * id: data.id, * title: data.getValue('name'), * }; * }); * ``` */ registerFactory(type: string, factory: ViewFactory): void; } /** * Factory function to create or transform a view from data * * View factories are pure functions that take input data and produce a view * representation. They can create new views from scratch or transform existing * views created by previous factories in the chain. * * Whatever a factory returns becomes the view the next factory receives, and * the result of the chain if it is the last one. To leave the view untouched, * return the `view` argument. Returning `undefined` does not skip the factory, * it clears the view for every factory after it. * * ## Never change the view you are given * * A factory owns only the object it returns. The `view` argument was built by * another factory, often one from another package, and that factory still * counts on the object being what it made. Writing to it changes data the * factory does not own, and the result then depends on the order factories were * registered in. The factory that made the change is rarely the one that looks * broken. * * Copy the view, change the copy, and return the copy. Do not assign to * `view.title`, and do not call `view.items.push(...)` or `view.items.sort()`. * A spread copies only the top level, so any nested object or array is still * the one the previous factory made. Build new nested values too, as the last * example below shows. * * To leave the view as it is, return the `view` argument. * * Nothing in the type stops a factory from changing the view today. The rule is * a convention, so a factory that breaks it still compiles. Keep views shaped * like plain data, which makes copying a nested value straightforward. * * The same holds for `data`. Read from it and never write to it. * * A factory should also check that `view` exists before spreading it, and * handle a single concern so that several can be combined. * * @param data - the data to create a view for, typically a {@link LimeObject} * @param scope - the {@link ContextScope} the view is rendered in. Bound * to the consumer's host element so values like the current context, * limeobject, user etc. resolve correctly. * @param view - the view as it was created by the previous factory, or * `undefined` if this is the first factory in the chain * * @example Creating a new view * ```ts * import { LimeObject, ViewFactory } from '@limetech/lime-web-components'; * * interface ListItem { * id: number; * title: string; * icon: string; * } * * const factory: ViewFactory = (data, scope, view) => { * return { * id: data.id, * title: data.getValue('name'), * icon: 'file', * }; * }; * ``` * * @example Transforming an existing view * ```ts * import { LimeObject, ViewFactory } from '@limetech/lime-web-components'; * * interface ListItem { * id: number; * title: string; * subtitle?: string; * } * * const factory: ViewFactory = (data, scope, view) => { * if (!view) { * return view; * } * * return { * ...view, * subtitle: data.getLimetype().localname.singular, * }; * }; * ``` * * @example Adding to a nested value without changing the original * ```ts * import { LimeObject, ViewFactory } from '@limetech/lime-web-components'; * * interface ListItem { * id: number; * title: string; * actions: string[]; * } * * const factory: ViewFactory = (data, scope, view) => { * if (!view) { * return view; * } * * return { * ...view, * // A spread only copies the top level, so `view.actions` is still the * // array the previous factory made. Build a new array from it instead * // of calling `view.actions.push('archive')`. * actions: [...view.actions, 'archive'], * }; * }; * ``` * * @example Conditional transformation * ```ts * import { LimeObject, ViewFactory } from '@limetech/lime-web-components'; * * interface ListItem { * id: number; * title: string; * icon?: string; * } * * const factory: ViewFactory = (data, scope, view) => { * if (!view || data.getLimetype().name !== 'company') { * return view; * } * * return { * ...view, * icon: 'building', * }; * }; * ``` * * @beta * @group Views */ export type ViewFactory = (data: TData, scope: ContextScope, view?: TView) => TView | undefined;