neocomp.js

Component class

neocomp is a component oriented framework, this means that it is build on the idea of components.

a component is a buillding unit that represet a chunk of the user interface, it wraps a HTMLElement and define logic and state associated to it.

components are typesafe, reactive, oraganizable, flixible and lightweight.

a component is a class that contains:

in general, the flow of control in a component is:

events (DOM / external)
 --(call)-> methods (logic) 
  --(update)-> states 
   --(trigger)-> effects 
    --(effect)-> DOM (static bindings)

why not a function

being a function requires you to construct the element synchronously, also it has inflixible organazation (only one function).

classes on the other hand can create the base synchronously and then perform asyncronous updates.
also logic can be organized into methods with shared states in properties.

functions are good in factory paradigm, but classes are the best for logicfull organized components, components that contains logic other than construction and simple binding.

TypeMap

export interface BaseMap {
	props: Record<string, any>,
	refs: Record<string, HTMLElement | HTMLElement[]>,
	childmap: Record<string, PureComp>,
	chunks: string
}

export type getTypeMap <Comp> = BaseMap;
export type getProps <Comp> = BaseMap['props'];
export type getRefs <Comp> = BaseMap['refs'];
export type getChildMap <Comp> = BaseMap['childmap'];
export type getChunks <Comp> = BaseMap['chunks'];

TypeMap: is an abstracted type that groups the types used in Component.

it must extends from BaseMap, the base of all TypeMap, and it can contains any other types.

TypeMap can contain

getTypeMap: extract the typemap of a given Component.

getProps, getRefs, getChildMap and getChunks: extracts the respectfull typemap field from a given Component.

example

interface TypeMap extends BaseMap {
	props: {
		title: string, count: number
	},
	refs: {
		input: HTMLElement, sections: HTMLElement[]
	}
}

interface TypeMap extends BaseMap {
	childmap: {
		sectionA: SectionA, sectionB: SectionB
	},
	chunks: 'item' | 'title' | 'minititle'
}

constructor and options

export class Component <TypeMap extends BaseMap> implements Linkable {
	constructor (el?: HTMLElement, initMode: 'core' | 'dom' | 'full' = 'core');
	options: CompOptions;
	static defaults: CompOptions;
	static template: Template;
	static chunks: Record<string, Template> = {};
}

export type CompOptions = {
	anonymous: boolean;
	defaultId: (comp: PureComp) => string;
	removeChildren: boolean;
	store: Partial<StoreOptions>;
	view: Partial<ViewOptions>;
}

constructor: take an optional HTMLElement and initMode, and construct a Component.
see initialization for more info.

options: is the CompOptions defined for this component.

defaults: is the default options defined for all instances of this component class.

template: is the default template for this component, defaults to empty <div>.

chunks: optional static property that defines the chunks used in this component.

CompOptions

example

class Example extends Component<TypeMap> {
	//overide default template
	static template = $template(`<div>hallo world</div>`);

	//define custom chunks
	static chunks = {
		item: $template(`<div class=item .text>item @(){context.index}</div>`),
	}

	static defaults = {
		// important
		...Component.defaults,

		//default id is 6 digit hexadecimal number
		defaultId: (comp) => Math.round(Math.random() * (16**6)).toString(16),

		//this component is a placeholder, dont notify the global systems
		anonymous: true,

		//preserve children
		removeChildren: false
	}
}

main metadata

export class Component {
	id: string = '';
	name: string = '';
	status: Status;
}

export type Status = 'coreInit' | 'domInit' | 'inited' | 'removing' | 'removed';

id: the identifier of the component, it is globaly unique, so it can be used as a key.

by default, it is the id of the element given at construction, else the result of options.defaultId, as a fallback, a 9 digit random number or name-xxx if name is given.

name: the human readable name of the component, can be used as a unique key at the local scale (near parent and children).

it is given by the neo:name attribute of the element given at construction, else empty string unless changed by the user at initialization.

status: the status of the component, it can be one of the following:

example

new Component() // id: random, name: ''

new Component(constructOne(`<div id=id neo:name=name></div>`)) // id: id, name: name

initialization

export class Component {
	elementArgs (): Record<string, any>;

	initDom (): void;
	fireInit (): void;
}

initialization is a very important step in component lifecycle, it defines the inital state, DOM structure and bindings, and interactions with the world.

it is done in the constructor of the derived Component classes, which have the following signature:

constructor (el?: HTMLElement, ...args: any[])

arguments

arguments can be passed through the constructor, or in template through .arg:name attributes, these are accesed through elementArgs method, callable only once.

constructor (el?: HTMLElement, a?: number, b?: string) {
	//...
	({ a, b } = { a: a || 0, b: b || '', ...this.elementArgs() });
}
new Comp(ConstructOne('<div .arg:b="b"></div>'), 1) // a: 1, b: 'b'

initialization sequence

a normal intialization sequence consitis of:

skips

certain initialization phases can be skipped after super(el) throught initMode parameter of the Component contructor, its values:

example

constructor (el?: HTMLElement, ...args) {
	super(el);
	// set initial state
	this.initDom(); //dom ready
	// code that use the dom
	this.fireInit(); //fully inited
	// async code / external system if required
}

constructor (el?: HTMLElement, ...args) {
	super(el, 'dom');
	// init code
	this.fireInit(); //fully inited
}

constructor (el?: HTMLElement, ...args) {
	super(el, 'full'); //no init code
}

initialization events

export class Component {
	onDomInit: OTIEvent<(comp: this) => void>;
	onInitInternal: OTIEvent<(comp: this) => void>;
	onInit: OTIEvent<(comp: this) => void>;
}

these events are for system use cases.

onDomInit: triggered when the initial DOM strucutre is constructed and before post initDom code.

onInitInternal: triggered when the component is fully inited, for internal systems before external notifying.

onInit: triggered when the component is fully init, for external use (hierarchy).

by convention, all external interaction with the component must be called within onInit. because, the initial component may be a placeholder for the true component, like in lazy loading.
for this, use the component passed by the event not the listened one.

example

const lazy = new (registry.get('@lazy:example'))(); //LazyComp
lazy.set('prop', value); //error
lazy.onInit.listen((comp) => { //ExampleComp
	comp.set('prop', value);
});

const comp = new ExampleComp();
comp.status; // => 'inited'
comp.onInit.listen((comp) => console.log('inited')) // => inited

removing

export class Component {
	onRemove: Event<(comp: this) => void>;
	remove (): void;
}

remove: removes the component and unlink it from everything (DOM, parent, children, global, links).

can be called multiple times safely while being in removing phase.

if you noticed a removed component in an unusual place, there is a memory leak.

onRemove: an event triggered on removing, use it for clean up.
there are other events specialized for external systems / hierarchy.

example

comp.onRemove.listen((comp) => {
	someInternals.remove(comp);

	comp.remove(); //noop, since status = 'removing'
});

comp.remove();

hierarchy

export class Component {
	parent?: PureComp;
	children: PureComp[];
	childmap: TypeMap['childmap'];
}

components has an optional parent children hierarchy.

in normal apps, the greatest grandfather is the root component.

all hierarchy interactions are done after init.

parent: the parent of the component, it is optional.

children: the children of the component.

childmap: a record that maps component children by their names.

linking

export class Component {
	addChild (child: PureComp, ind: number = -1): void;
	linkParent (parent: PureComp): void;
	onChildAdded: Event<(comp: this, child: PureComp) => void>;
	onAddedToParent: Event<(comp: this, parent: PureComp) => void>;
}

addChild: add a child to a component at a given index, by default at the end.
the index can be negative, it doesnt append the child top element.

linkParent: link the passed component as parent to the component.

addChild calls linkParent on the child, while linkParent doesnt add the child to the parent.
in normal cases use addChild.

onChildAdded: an event triggered when a child of the component is added.

onAddedToParent: an event triggered when a parent of the component is linked.

example

const parent = new Component(); //chilren: [], childmap: {}

parent.addChild(new Component(constructOne('<div neo:name=child1></div>'))); 
	//children: [child1], childmap: { child1: child1 }

parent.addChild(new Component(constructOne('<div neo:name=child2></div>')), 0); 
	//children: [child2, child1], childmap: { child1: child1, child2: child2 }

unlinking

export class Component {
	unlinkParent (): void;
	unlinkChild (child: PureComp) :void;

	onChildUnlink: Event<(comp: this, child: PureComp) => void>;
	onUnlinkedFromParent: Event<(comp: this, parent: PureComp) => void>;
}

unlinkParent: unlink the parent of the component.

unlinkChild: unlink the given child from component.

unlickParent calls unlinkChild on the parent, while unlinkChild doesnt unlink the parent from the child.
in normal cases use unlinkParent.

remove also unlink the parent and children.

onUnlinkedFromParent: an event triggered when the parent of the component is unlinked.

onChildUnlink: an event triggered when a child of the component is unlinked.

use these events for cleanup instead of onRemove, they are called on component removal.

example

const parent = /* */; //chilren: [child1, child2], childmap: { child1: child1, child2: child2 }

parent.childmap.child1.unlinkParent(); //chilren: [child2], childmap: { child2: child2 }

parent.childmap.child2.remove(); //chilren: [], childmap: {}

linking

export class Component {
	onLink: Event<(comp: this, linked: Linkable) => void>;
	onUnlink: Event<(comp: this, unlinked: Linkable) => void>;
	link (other: Linkable): void;
	unlink (other: Linkable): void;
	hasLink (other: Linkable): boolean;
}

Component implements the Linkable interface, read linkable section for more info.

use onUnlink eventea for cleanup insted of onRemove, it is called on component removal.

remove unlink the linked linkables.

example

const comp = new Component();
const context1 = new Context();
const context2 = new Context();

link(comp, context1);
link(comp, context2);

unlink(comp, context1); //unlinked manually

comp.remove(); //context2 unlinked automatically

store

export class Component {
	store: Store<TypeMap['props']>;
	get <P extends keyof TypeMap['props']> (name: P | symbol): TypeMap['props'][P]
	set <P extends keyof TypeMap['props']> (name: P | symbol, value: TypeMap['props'][P]): void;
	setMultiple (props: Partial<TMap['props']>): void;
	signal <P extends keyof TypeMap['props']> (name: P | symbol, Default?: TypeMap['props'][P]):
	  Signal<TypeMap['props'][P]>;
	computed <P extends keyof TMap['props']> (
		name: P | symbol, effectedBy: EffectedProp<TMap['props']>[] | 'track', fn: () => TMap['props'][P]
	): ReadOnlySignal<Prop<P>>;
	effect (
		effectedBy: EffectedProp<TMap['props']>[], handler: () => void,
		effect?: EffectedProp<TMap['props']>[]
	): void;
	effect (track: 'track', handler: () => void): void;
}

every Component has a Store that contains the state.

get and set: get and set a given property.

setMultiple: set multiple properties at once.

signal: creates a Signal of a given property.

computed: creates a computed property and returns a ReadOnlySignal of it.

effect: add an effect effected by and effecting a given properties.

for more details read state fundamentals

example

comp.set('a', 1);
comp.get('a') // => 1

comp.setMultiple({ b: 2, c: 3 });

const a = comp.signal('a');

const d = comp.computed('d', 'track', () => a.value + 1); // => 2

comp.effect('track', () => console.log(a.value, d.value));

a.value = 2; // => 2, 3

view

export class Component {
	view: View<TypeMap['refs'], TypeMap['chunks']>;
	el: HTMLElement;
	refs: Record<keyof TypeMap['refs'], HTMLElement[]>;
	query <T extends HTMLElement = HTMLElement> (selector: string): T[];
	chunk (chunk: TMap['chunks'] | Template, context?: Record<string, any>): HTMLElement; 
}

export const attachedComp: symbol;

every Component has a View responsible for managing the DOM.

el: represents the top element that the component wraps.

can be the element passed to the component on construction, else an element created by view.options.defaultEl

refs: the references to elements created by @ref action attribute.

query: return all elements in the top element that match the given selector.

chunk: construct a given chunk optional passed with context.

attachedComp: a symbol added to top element where its value refers to the attached component.

example

class ExampleComponent extends Component<TypeMap> {
	static template = $template(`<span @ref=hello>hello</span> <span>world</span>`)
	static chunk = { hello: $template(`<span .text>hello @(){context.to}</span>`) }
}
const comp = new ExampleComponent(constructOne(`<div id=comp></div>`));

comp.el; // => <div id=comp></div>

comp.query('span'); // => [<span>hello</span>, <span>world</span>]

comp.refs.hello; // => <span>hello</span>

comp.chunk('hello', { to: 'world' }); // => <span>hello world</span>

component variants

export type PureComp = Component<BaseMap>;

PureComp: is any Component, it is used to interact with components of any kind.