import {Html} from "../dom";
import {Container, Inject} from "../container";
import {TemplateManager} from "../template";
import {GetString, Strings, ToString} from "../utils";
import {Collection, HashMap} from "../collection";
import {Reactor, ReactorManager, Reactors} from "../reactor";
import {NATIVE_PROPERTY, NativeObject} from "./native";
import {REACT_PROPERTY, ReactProperty} from "./react";
import {EventBus, EventManager} from "../event";
import {SCOPED_EVENT_PROPERTY, ScopedEventObject} from "./scoped-event";
export type ParentComponent = Component | null;
export class Component {
@Inject(Container)
protected container: Container;
@Inject(TemplateManager)
protected templateManager: TemplateManager;
@Inject(ReactorManager)
protected reactorManager: ReactorManager;
protected parentComponent: ParentComponent;
protected components: Collection;
protected reactors: HashMap;
protected eventManager: EventManager;
protected html: Html;
public constructor() {
this.components = new Collection();
this.reactors = new HashMap();
}
public parent(parent: ParentComponent): Component {
this.parentComponent = parent;
return this;
}
public render(): Html {
this.render = () => {
throw new Error(`Cannot render component [${GetString(this)}] multiple times; please create a new instance.`);
};
this.html = this.templateManager.render(GetString(this));
this.resolveComponents();
this.resolveScopedEvents();
this.resolveBinds();
this.resolveNatives();
return this.html = this.onRender();
}
/**
* register a new state to this component
*
* @param reactors
*/
public register(reactors: Reactors): Component {
this.unsubscribe();
this.reactors.copy(new HashMap(reactors));
this.subscribe();
return this;
}
/**
* set a new state and update the component by executing the state
*
* @param reactors
*/
public update(reactors: Reactors = {}): Component {
this.register(reactors);
this.reactors.each((_, reactor) => reactor.update());
return this;
}
public reactor(name: string = 'default'): Reactor {
return this.reactors.require(name, `Requested unknown reactor [${name}] on component [${GetString(this)}].`);
}
public push(name: string, object: Object): Component {
this.reactor(name).push(object);
return this;
}
public patch(name: string, object: Object): Component {
this.reactor(name).patch(object);
return this;
}
public isRendered(): boolean {
return !!this.html;
}
public subscribe(): Component {
new Collection((this)[REACT_PROPERTY]).each((reactProperty: ReactProperty) => {
if (this.reactors.has(reactProperty.reactor)) {
this.reactors.require(reactProperty.reactor).subscribe(reactProperty.property, (this)[reactProperty.handler].bind(this));
}
});
return this;
}
public unsubscribe(): Component {
new Collection((this)[REACT_PROPERTY]).each((reactProperty: ReactProperty) => {
if (this.reactors.has(reactProperty.reactor)) {
this.reactors.require(reactProperty.reactor).unsubscribe(reactProperty.property, (this)[reactProperty.handler]);
}
});
return this;
}
public component(bind: ToString): U {
return this.container.create(bind.toString());
}
/**
* called when a component should be ready to use
*/
public wakeup(): Component {
this.onWakeup();
this.subscribe();
return this;
}
/**
* called when a component is going to sleep (component should not be destroyed);
* component no longer registers any events (include state changes)
*/
public sleep(): Component {
this.onSleep();
this.unsubscribe();
return this;
}
public root(): Component {
let component: Component = this;
while (component.parentComponent) {
component = component.parentComponent;
}
return component;
}
public getEventManager(): EventManager {
return this.eventManager || (this.eventManager = this.container.autowire(new EventManager()));
}
public scope(scope: string): EventBus {
return this.root().getEventManager().scope(scope);
}
/**
* the magic of component tree - this method creates other components (and triggers render method on them)
*/
protected resolveComponents(): void {
this.html.selectorCollection('[data-component]').each(html => {
const bind = html.attr('data-bind');
const component = this.components.addi(this.container.create(html.rattr('data-component')).parent(this));
if (bind) {
(this)[Strings.toCamelCase(bind)] = component;
}
html.replaceBy(component.render());
});
}
protected resolveScopedEvents(): void {
const eventManager = this.root().getEventManager();
new Collection((this)[SCOPED_EVENT_PROPERTY]).each(scopedEventProperty => {
eventManager.scope(scopedEventProperty.scope).listener(scopedEventProperty.event).add(
(this)[scopedEventProperty.handler],
scopedEventProperty.weight,
this,
scopedEventProperty.cancellable
);
});
}
/**
* links are basically same as mounts, but they're directly put into properties of this component (converting foo-bar to fooBar convention)
*/
protected resolveBinds(): void {
this.html.selectorCollection('[data-bind]').each(html => {
(this)[Strings.toCamelCase(html.rattr('data-bind'))] = html.removeAttr('data-bind');
});
}
protected resolveNatives(): void {
new Collection((this)[NATIVE_PROPERTY]).each(nativeProperty => {
(nativeProperty.callback ? nativeProperty.callback.call(this, this) : this.html).listenTo(nativeProperty.event, (this)[nativeProperty.handler].bind(this));
});
}
protected onRender(): Html {
return this.html;
}
protected onWakeup(): void {
}
protected onSleep(): void {
}
}