asciitorium
Version:
an ASCII CLUI framework
166 lines (165 loc) • 6.62 kB
JavaScript
import { Component } from '../core/Component.js';
import { Case } from './Case.js';
import { Default } from './Default.js';
/**
* Creates a Component instance from a factory function.
*
* @param factory The factory function that returns a Component instance
* @returns Component instance or undefined if creation fails
*/
function makeComponent(factory) {
if (typeof factory !== 'function') {
return undefined;
}
const result = factory();
if (result instanceof Component) {
return result;
}
return undefined;
}
/**
* Switch component that displays a child component based on a reactive state.
*
* Inspired by React's conditional rendering patterns, this component provides
* a clean way to dynamically switch between different components based on state.
*
* Two usage modes:
*
* 1. Factory function mode:
* ```tsx
* const currentView = new State(() => new DashboardView({ width: 100 }));
* <Switch component={currentView} />
* ```
*
* 2. Condition-based mode with Case/Default children (recommended):
* ```tsx
* const userRole = new State<string>("admin");
* <Switch condition={userRole}>
* <Case when="admin"><AdminPanel /></Case>
* <Case when="user"><UserPanel /></Case>
* <Default><GuestPanel /></Default>
* </Switch>
* ```
*/
export class Switch extends Component {
constructor(props) {
// Validation
if (props.component && props.condition) {
throw new Error('Switch cannot have both component and condition props');
}
if (!props.component && !props.condition) {
throw new Error('Switch must have either component or condition prop');
}
// Prepare props: if condition mode, extract and remove children
let processedProps = props;
let extractedCases = [];
let extractedDefault;
if (props.condition) {
// Extract Case and Default components from props.children
const children = props.children ? (Array.isArray(props.children) ? props.children : [props.children]) : [];
extractedCases = children.filter((c) => c instanceof Case);
extractedDefault = children.find((c) => c instanceof Default);
// Remove children from props so super() doesn't add them
processedProps = { ...props, children: undefined };
}
// Call super with processed props
super(processedProps);
this.cases = [];
// Set up based on mode
if (props.condition) {
// Condition mode
this.cases = extractedCases;
this.defaultCase = extractedDefault;
this.conditionState = props.condition;
this.bind(this.conditionState, () => this.updateContent());
}
else {
// Legacy component mode
this.componentState = props.component;
this.bind(this.componentState, () => this.updateContent());
}
// Set initial content
this.updateContent();
}
/**
* Override setChildren to capture Case and Default components in condition mode
*/
setChildren(children) {
if (this.conditionState) {
// Extract Case and Default components
this.cases = children.filter((c) => c instanceof Case);
this.defaultCase = children.find((c) => c instanceof Default);
// Update content based on current condition
this.updateContent();
}
else {
// Legacy component mode: just set children normally
super.setChildren(children);
}
}
/**
* Updates the displayed content based on the current state value.
* Clears existing children and creates the appropriate component.
*/
updateContent() {
// Remove all existing children from Switch
const childrenToRemove = [...this.children];
for (const child of childrenToRemove) {
child.destroy();
this.removeChild(child);
}
// Condition mode: find matching Case or Default
if (this.conditionState) {
const currentCondition = this.conditionState.value;
const matchingCase = this.cases.find(c => c.when === currentCondition);
if (matchingCase) {
// Check if Case has a component factory
const factory = matchingCase.getComponentFactory();
if (factory) {
// Create new instance from factory
const component = makeComponent(factory);
if (component) {
this.addChild(component);
}
}
else {
// Fallback: reuse existing children (NOT RECOMMENDED - components stay alive)
for (const child of matchingCase.getChildren()) {
console.warn('Switch: Case component missing create prop. Components will persist in memory and continue running. Use: <Case when="' + matchingCase.when + '" create={' + child.constructor.name + '} />');
this.addChild(child);
}
}
}
else if (this.defaultCase) {
// Check if Default has a component factory
const factory = this.defaultCase.getComponentFactory();
if (factory) {
// Create new instance from factory
const component = makeComponent(factory);
if (component) {
this.addChild(component);
}
}
else {
// Fallback: reuse existing children (NOT RECOMMENDED - components stay alive)
console.error('Switch: Default component missing create prop. Components will persist in memory and continue running. Use: <Default create={YourComponent} />');
for (const child of this.defaultCase.getChildren()) {
this.addChild(child);
}
}
}
}
// Legacy component mode
else if (this.componentState) {
const entry = this.componentState.value;
if (entry) {
const component = makeComponent(entry);
if (component) {
this.addChild(component);
}
}
}
// Notify the app that the focus tree has changed and needs reset
this.notifyAppOfFocusReset();
}
}