Skip to content

Commit

Permalink
chore(typechecking): fix typechecking on smart
Browse files Browse the repository at this point in the history
  • Loading branch information
pdesoyres-cc committed Sep 2, 2024
1 parent 9fa8b2a commit e23f12f
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 73 deletions.
93 changes: 50 additions & 43 deletions src/lib/define-smart-component.js
Original file line number Diff line number Diff line change
@@ -1,45 +1,26 @@
import { produce } from './immer.js';
import { defineSmartComponentCore } from './smart-manager.js';

const META = Symbol('META');

/**
* @typedef {Element} SmartContainer
* @property {Object} context
*/

/**
* @typedef {Object|((property: Object) => void)} CallbackOrObject
*/
import { META } from './smart-symbols.js';

/**
* @typedef {Object} SmartDefinitionParam
* @property {TypeHint} type
* @property {boolean} [optional=false]
* @template TypeHint
* @typedef {import('./smart-component.types.js').SmartContainer} SmartContainer
* @typedef {import('./smart-component.types.js').SmartComponent} SmartComponent
* @typedef {import('./smart-component.types.js').SmartComponentDefinition} SmartComponentDefinition
* @typedef {import('./smart-component.types.js').SmartContext} SmartContext
* @typedef {import('./smart-component.types.js').OnEventCallback} OnEventCallback
* @typedef {import('./smart-component.types.js').CallbackOrObject} CallbackOrObject
* @typedef {import('./smart-component.types.js').UpdateComponentCallback} UpdateComponentCallback
*/

/**
* @param {Object} definition
* @param {string} definition.selector
* @param {{[name: string]: SmartDefinitionParam}} definition.params
* @param {({
* container?: SmartContainer,
* component?: Element,
* context?: Object,
* onEvent?: (type: string, listener: (detail: Object) => void) => void,
* updateComponent?: (
* type: string,
* callbackOrObject: CallbackOrObject,
* ) => void,
* signal?: AbortSignal,
* }) => void} definition.onContextUpdate
*
* @param {SmartComponentDefinition} definition
*/
export function defineSmartComponent(definition) {
defineSmartComponentCore({
selector: definition.selector,
params: definition.params,
onConnect(container, component) {
onConnect(_container, component) {
if (component[META] == null) {
component[META] = new Map();
}
Expand Down Expand Up @@ -68,11 +49,14 @@ export function defineSmartComponent(definition) {

// Prepare a helper function to attach event listeners on the component
// and make sure they're removed if the signal is aborted

/** @type {OnEventCallback} */
function onEvent(type, listener) {
component.addEventListener(
type,
(e) => {
listener(e.detail);
const event = /** @type {CustomEvent} */ (e);
listener(event.detail);
},
{ signal },
);
Expand All @@ -87,30 +71,53 @@ export function defineSmartComponent(definition) {

target.addEventListener(
'update-component',
(event) => {
const { property, callbackOrObject } = event.data;
if (typeof callbackOrObject === 'function') {
if (component[property] != null) {
component[property] = produce(component[property], callbackOrObject);
}
} else {
component[property] = callbackOrObject;
}
(e) => {
const event = /** @type {UpdateComponentEvent} */ (e);
handleUpdateComponent(component, event.property, event.callbackOrObject);
},
{ signal },
);

/** @type {UpdateComponentCallback} */
function updateComponent(property, callbackOrObject) {
const event = new Event('update-component');
event.data = { property, callbackOrObject };
const event = new UpdateComponentEvent(property, callbackOrObject);
target.dispatchEvent(event);
}

definition.onContextUpdate({ container, component, context, onEvent, updateComponent, signal });
},
onDisconnect(container, component) {
onDisconnect(_container, component) {
component[META].get(definition).abortController?.abort();
component[META].delete(definition);
},
});
}

class UpdateComponentEvent extends Event {
/**
* @param {string} property
* @param {CallbackOrObject} callbackOrObject
*/
constructor(property, callbackOrObject) {
super('update-component');
this.property = property;
this.callbackOrObject = callbackOrObject;
}
}

/**
*
* @param {SmartComponent} component
* @param {string} property
* @param {CallbackOrObject} callbackOrObject
*/
function handleUpdateComponent(component, property, callbackOrObject) {
const c = /** @type {{[p:property]: any}} */ (component);
if (typeof callbackOrObject === 'function') {
if (c[property] != null) {
c[property] = produce(c[property], callbackOrObject);
}
} else {
c[property] = callbackOrObject;
}
}
48 changes: 48 additions & 0 deletions src/lib/smart-component.types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { CcSmartContainer } from '../components/cc-smart-container/cc-smart-container.js';
import { COMPONENTS, CURRENT_CONTEXT, LAST_CONTEXT, META } from './smart-symbols.js';

export interface SmartContainer extends CcSmartContainer {
[COMPONENTS]?: Map<SmartComponentCoreDefinition, Array<SmartComponent>>;
[CURRENT_CONTEXT]?: SmartContext;
}

export interface SmartComponent extends Element {
[LAST_CONTEXT]?: SmartContext;
[META]?: Map<SmartComponentDefinition, { abortController?: AbortController }>;
}

export interface SmartComponentCoreDefinition {
selector: string;
params: Record<string, SmartDefinitionParam>;
onConnect: (container: SmartContainer, component: SmartComponent) => void;
onContextUpdate: (container: SmartContainer, component: SmartComponent, context: SmartContext) => void;
onDisconnect: (container: SmartContainer, component: SmartComponent) => void;
}

export interface SmartComponentDefinition {
selector: string;
params: Record<string, SmartDefinitionParam>;
onContextUpdate: (args: OnContextUpdateArgs) => void | Promise<void>;
}

export interface SmartDefinitionParam<TypeHint = unknown> {
type: TypeHint;
optional?: boolean;
}

export type SmartContext = Record<string, any>;

export type OnEventCallback = (type: string, listener: (detail: any) => void) => void;

export type UpdateComponentCallback<T = unknown> = (propertyName: string, property: CallbackOrObject<T>) => void;

export interface OnContextUpdateArgs {
container: SmartContainer;
component: SmartComponent;
context: SmartContext;
signal: AbortSignal;
onEvent: OnEventCallback;
updateComponent: UpdateComponentCallback;
}

export type CallbackOrObject<T = unknown> = T | ((property: T) => void);
59 changes: 29 additions & 30 deletions src/lib/smart-manager.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { COMPONENTS, CURRENT_CONTEXT, LAST_CONTEXT } from './smart-symbols.js';
import { objectEquals } from './utils.js';

/**
* @typedef {import('./smart-component.types.js').SmartContainer} SmartContainer
* @typedef {import('./smart-component.types.js').SmartComponent} SmartComponent
* @typedef {import('./smart-component.types.js').SmartComponentCoreDefinition} SmartComponentCoreDefinition
* @typedef {import('./smart-component.types.js').SmartContext} SmartContext
*/

// In the global space of this module (for any module importing it), we maintain:
// * a rootContext object
// * a smartContainers Set with all containers currently in the page
Expand All @@ -12,28 +20,16 @@ import { objectEquals } from './utils.js';
// On each component, we maintain:
// * an object with the last context on component[LAST_CONTEXT]

/**
* @typedef SmartComponentDefinition
* @property {String} selector
* @property {Object} [params]
* @property {Function} [onConnect]
* @property {Function} [onContextUpdate]
* @property {Function} [onDisconnect]
*/

const COMPONENTS = Symbol('COMPONENTS');
const CURRENT_CONTEXT = Symbol('CURRENT_CONTEXT');
const LAST_CONTEXT = Symbol('LAST_CONTEXT');
let rootContext = {};

/** @type {Set<CcSmartContainer>} */
/** @type {Set<SmartContainer>} */
const smartContainers = new Set();

/** @type {Set<SmartComponentDefinition>} */
/** @type {Set<SmartComponentCoreDefinition>} */
const smartComponentDefinitions = new Set();

/**
* @param {CcSmartContainer} container
* @param {SmartContainer} container
* @param {AbortSignal} signal
*/
export function observeContainer(container, signal) {
Expand Down Expand Up @@ -62,7 +58,7 @@ export function observeContainer(container, signal) {
}

/**
* @param {SmartComponentDefinition} definition
* @param {SmartComponentCoreDefinition} definition
* @param {AbortSignal} [signal]
*/
export function defineSmartComponentCore(definition, signal) {
Expand All @@ -88,16 +84,19 @@ export function defineSmartComponentCore(definition, signal) {
}

function updateEverything() {
/** @type {Array<[SmartContainer, SmartComponentCoreDefinition, SmartComponent]>} */
const allDisconnectedComponents = [];
/** @type {Array<[SmartContainer, SmartComponentCoreDefinition, SmartComponent]>} */
const allConnectedComponents = [];
/** @type {Array<[SmartContainer, SmartComponentCoreDefinition, SmartComponent]>} */
const allIdleComponents = [];

smartContainers.forEach((container) => {
smartComponentDefinitions.forEach((definition) => {
/** @type {Array<SmartComponent>} */
const allDefinitionComponents = Array.from(container.querySelectorAll(definition.selector)).filter(
(c) => closestParent(c, 'cc-smart-container') === container,
);

const previousComponents = container[COMPONENTS].get(definition) ?? [];
container[COMPONENTS].set(definition, allDefinitionComponents);

Expand Down Expand Up @@ -132,7 +131,7 @@ function updateEverything() {
}

/**
* @param {CcSmartContainer} container
* @param {SmartContainer} container
* @param {Object} context
*/
export function updateContext(container, context) {
Expand All @@ -149,19 +148,19 @@ export function updateRootContext(context) {
}

/**
* @param {CcSmartComponent} container
* @param {SmartComponentDefinition} definition
* @param {Element} component
* @param {SmartContainer} container
* @param {SmartComponentCoreDefinition} definition
* @param {SmartComponent} component
*/
function connectComponent(container, definition, component) {
component[LAST_CONTEXT] = {};
definition.onConnect?.(container, component);
}

/**
* @param {CcSmartComponent} container
* @param {SmartComponentDefinition} definition
* @param {Element} component
* @param {SmartContainer} container
* @param {SmartComponentCoreDefinition} definition
* @param {SmartComponent} component
*/
function updateComponentContext(container, definition, component) {
const currentContext = { ...rootContext, ...container[CURRENT_CONTEXT] };
Expand All @@ -174,9 +173,9 @@ function updateComponentContext(container, definition, component) {
}

/**
* @param {CcSmartComponent} container
* @param {SmartComponentDefinition} definition
* @param {Element} component
* @param {SmartContainer} container
* @param {SmartComponentCoreDefinition} definition
* @param {SmartComponent} component
*/
function disconnectComponent(container, definition, component) {
delete component[LAST_CONTEXT];
Expand All @@ -197,8 +196,8 @@ function closestParent(element, selector) {

/**
*
* @param {Object|null} source
* @param {Object|null} keyObject
* @param {SmartContext|null} source
* @param {SmartContext|null} keyObject
* @return {Object}
*/
function filterContext(source, keyObject) {
Expand All @@ -210,6 +209,6 @@ function filterContext(source, keyObject) {
}
const newEntries = Object.keys(keyObject)
.map((name) => [name, source[name]])
.filter(([name, value]) => value !== undefined);
.filter(([, value]) => value !== undefined);
return Object.fromEntries(newEntries);
}
4 changes: 4 additions & 0 deletions src/lib/smart-symbols.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const COMPONENTS = Symbol('COMPONENTS');
export const CURRENT_CONTEXT = Symbol('CURRENT_CONTEXT');
export const LAST_CONTEXT = Symbol('LAST_CONTEXT');
export const META = Symbol('META');

0 comments on commit e23f12f

Please sign in to comment.