import Component from "jwidget/Component";
Base class of UI component. Supports the next features:
jWidget component has very simple API, but pretty unusual philosophy to build up Model-View architecture without a lot of effort. Let's start with examples.
@template( '<div jwclass="my-component">' + '<div jwid="hello-message"></div>' + '<a href="#" jwid="link">Click me!</a>' + '</div>' ) class MyComponent extends Component { constructor(private message: string, private link: string) { super(); } protected afterRender() { super.afterRender(); this.getElement("hello-message").text(this.message); this.getElement("link").attr("href", this.link); } }
Let's find out how the HTML template works. Each component has a template, that is passed into template annotation and defaults to <div></div>. Subclass inherits the superclass template.
Pay attention to special attributes jwclass and jwid in the template. jwclass is a root CSS class of the component, and jwid is a suffix to jwclass in this element. So, the next HTML snippet is created in DOM as a result of this component rendering:
<div class="my-component"> <div class="my-component-hello-message"></div> <a href="#" class="my-component-link">Click me!</a> </div>
You can obtain an element by its jwid via method getElement. The result of this method is a jQuery wrapper over this element. The root element always has jwid = "root". In addition, the root element of the component is stored in el property of the component.
Component can be created by simple construction of component object. After that, you can use one of the methods render, renderTo, renderAs to render this component and optionally insert it into DOM.
$(() => { new MyComponent("Hello, World!", "javascript:alert('Hello!')").renderTo("body"); });
There are 5 ways to add a child component (note: examples are not complete - see Component removal and destruction.
Add a child component into children map with a key equal to jwid of the element to replace with the child component. Usually it is done in afterRender method.
afterRender() { super.afterRender(); this.children.set("label", new LabelView("Hello")); }
Add a bindable child component using addBindable method. Pass an instance of Bindable<Component> as an argument and the component will provide the synchronization with this property during application running.
afterRender() { super.afterRender(); this.contentView = new Property(new LabelView("Hello")); this.addBindable(this.contentView, "label"); } changeLabel(value: string) { this.contentView.set(new LabelView(value)); }
Add an array of child components into some element using addArray method. Pass an instance of ReadonlyBindableArray<Component> as an argument and the component will provide the synchronization with this array during application running.
afterRender() { super.afterRender(); this.labelViews = new BindableArray([new LabelView("one"), new LabelView("two")]); this.addArray(this.labelViews, "labels"); } addLabel(value: string) { this.labelViews.add(new LabelView(value)); }
Add a set of child components into some element using addSet method. As opposed to addArray method, addSet doesn't let you control the order of child components. A newly added component is always appended to the end. Framework will provide the synchronization with this set during application running.
afterRender() { super.afterRender(); this.labelViews = new BindableSet([new LabelView("one"), new LabelView("two")]); this.addSet(this.labelViews, "labels"); } addLabel(value: string) { this.labelViews.add(new LabelView(value)); }
Define method render<ChildId>, where <ChildId> is jwid of an element in CamelCase with capitalized first letter. Example: renderArticle renders the element with jwid = "article". If the method returns an instance of Component, Bindable, ReadonlyBindableArray or ReadonlyBindableSet, then result will be treated as a child component or a child component array/collection. Define method renderRoot to render the root element, but you can return only ReadonlyBindableArray or ReadonlyBindableSet there, because it is impossible to replace the root element of the component.
protected renderLabel() { return new LabelView("Hello"); }
See More about renderChild methods paragraph for details.
Such API provides simplicity, at one hand, and flexibility in Model-View regard, at another hand.
Reference: Tutorial. Part 1. Model and view
It is convenient to use startMappingArray and startMappingSet functions to convert data collections to UI component collections. Thanks to them, view is updated on data update automatically.
That's the reason why we recommend to use jWidget collections in data model instead of native JavaScript Array and Set: jWidget collections can be synchronized to each other.
Reference: Tutorial. Part 6. Collection bindings
You can define method render<ChildId> for any element in HTML template that has attribute jwid. <ChildId> equals to this jwid, written in CamelCase with capitalized first letter. Method signature:
protected renderChildId(el: JQuery): Renderable
Depending on the returned value of this method, you have the next options:
The method should be protected, not private, to avoid "unused method" warning of TypeScript compiler.
You can destroy a component via destroy method. However you can not destroy a component which is added into another component as a child (component throws an exception in this case). You must remove the child component from its parent first. To remove the component from its parent, you must perform the operation opposite to the adding operation.
As soon as a child component is removed, you can destroy it:
this.children.delete("comments").destroy();
For collections, you should do something like this (note: this code is not optimal, see the example below):
// should be called after the rendering initiation initLabels() { // Map label data array to view array this._labelViews = startMappingArray(this.labels, label => new LabelView(label), {destroy}); // Add labels into element with jwid="labels" this._labelArray = this.addArray(this._labelViews, "labels"); } clearLabels() { this._labelArray.destroy(); this._labelArray = null; this._labelViews.destroy(); this._labelViews = null; }
You don't need to remove the child components explicitly all the time. On parent component destruction, it automatically removes all the children before unrender method call. However, it doesn't destroy them. You can use aggregation method own to destroy child components. So, usually your code will look as simple as this:
renderTitleBox() { return this.own(new TitleBox()); } renderLabels() { return this.own(startMappingArray(this.labels, label => new LabelView(label), {destroy})); }
This example describes how to create and destroy a child component with jwid="title-box".
@template( '<div jwclass="my-component">' + '<div jwid="title-box"></div>' + '</div>' ) class MyComponent extends Component { protected renderTitleBox() { return this.own(new TitleBox()); } }
This example describes how to create and destroy a bindable child component with jwid="document". Assume that you have an observable property "document" and want to replace an old document view with a new one on document change.
@template( '<div jwclass="my-component">' + '<div jwid="document"></div>' + '</div>' ) class MyComponent extends Component { constructor(private document: Bindable<Document>) { super(); } protected renderDocument() { return this.own(this.document.map(document => new DocumentView(document), {destroy})); } }
This example describes how to create and destroy child components by data collection, and insert them into element with jwid="labels". If data collection is not silent, child component collection gets constantly synchronized with the data.
@template( '<div jwclass="my-component">' + '<div jwid="labels"></div>' + '</div>' ) class MyComponent extends Component { constructor(private labels: ReadonlyBindableArray<Label>) { super(); } protected renderLabels() { return this.own(startMappingArray(this.labels, label => new LabelView(label), {destroy)); } }
This example describes how to insert child components which have lifetime controlled by someone else, and therefore shouldn't be destroyed by parent component.
@template( '<div jwclass="my-component">' + '<div jwid="title-box"></div>' + '</div>' ) class MyComponent extends Component { constructor(private titleBox: Renderable) { super(); } protected renderTitleBox() { return this.titleBox; } }
Each component has several stages of life.
There's an easy way to attach HTML templates via WebPack. The first example from this topic can be splitted into two files:
@template(require<string>("./MyComponent.jw.html")) class MyComponent extends Component { constructor(private message: string, private link: string) { super(); } protected afterRender() { super.afterRender(); this.getElement("hello-message").text(this.message); this.getElement("link").attr("href", this.link); } }
<div jwclass="my-component"> <div jwid="hello-message"></div> <a href="#" jwid="link">Click me!</a> </div>
To make this work, you need to install html-loader NPM module:
npm install --save-dev html-loader
And use it in WebPack configuration:
module: { rules: [ // ... { test: /\.html$/, loader: "html-loader", query: {minimize: true, attrs: false} } ] }
new Component()
Constructs a new instance of Component.
readonly parent: Component
Parent component. The field is available from component rendering beginning.
readonly el: JQuery
Root element. The field is available from component rendering beginning.
readonly children: IBindableMap<Component>
Child component mutable map. Use this map to add child components in place of elements with corresponding jwid. The field is available from component rendering beginning.
readonly template: AbstractTemplate
Component template. Template is defined by template annotation.
(): this
Renders the component. Call this method to initialize references to all elements of component and create child components. This method is called automatically in the next cases:
Feel free to call component rendering multiple times: it gets rendered only once.
(el: string | HTMLElement | JQuery): this
Renders the component into an element. Use it to render root component only: children must be rendered using children, addArray, addSet, addBindable members.
(el: string | HTMLElement | JQuery): this
Renders the component in place of an element. Use it to render root component only: children must be rendered using children, addArray, addSet, addBindable members.
(): this
Removes the component from DOM. Can be used for root component only (which was added via renderTo or renderAs method). All child components should be removed using children map or binding deletion. See Component removal and destruction for details.
(id: string): this
Removes element by jwid. Element gets removed from DOM and destroyed. It is then impossible to get it by getElement method.
(component: Bindable<Component>, id: string): Destroyable
Adds child component and synchronizes the component with the property. On every property change, removes the child and adds another one. Equivalent to returning a Bindable<Component> instance in rendering method. It is convenient to create component property from data property using map method:
protected afterRender() { super.afterRender(); const avatarView = this.own(this.user.avatar.map(avatar => new AvatarView(avatar), {destroy})); this.addBindable(avatarView, "avatar"); }
addBindable method returns a binding. If you destroy it, the child gets removed from parent component and the synchronization gets stopped. The same happens when the component gets destroyed - right before unrender method call. But notice that child component inside this property does not get destroyed automatically. Usually it can be done by corresponding Mapper or Property destruction in unrender method or via own.
(source: ReadonlyBindableArray<Component>, el?: string | HTMLElement | JQuery): Destroyable
Adds an array of child components and synchronizes the component with it. On every array change, adds or removes corresponding children. Equivalent to returning a ReadonlyBindableArray<Component> instance in rendering method. As opposed to addSet method, retains component order. It is convenient to create "source" array from data array using startMappingArray utility:
protected afterRender() { super.afterRender(); const array = this.own(startMappingArray(this.users, user => new UserView(user), {destroy})); this.addArray(array, "users"); }
addArray method returns a binding. If you destroy it, the children get removed from parent component and the synchronization gets stopped. The same happens when the component gets destroyed - right before unrender method call. But notice that child components inside this array are not destroyed automatically. Usually it can be done by corresponding ArrayMapper or array destruction in unrender method or via own.
(source: ReadonlyBindableSet<Component>, el?: string | HTMLElement | JQuery): Destroyable
Adds a set of child components and synchronizes the component with it. On every set change, adds or removes corresponding children. Equivalent to returning a ReadonlyBindableSet<Component> instance in rendering method. As opposed to addArray method, ignores component order. It is convenient to create "source" collection from data collection using startMappingSet utility:
protected afterRender() { super.afterRender(); const set = this.own(startMappingSet(this.users, user => new UserView(user), {destroy})); this.addSet(set, "users"); }
addSet method returns a binding. If you destroy it, the children get removed from parent component and the synchronization gets stopped. The same happens when the component gets destroyed - right before unrender method call. But notice that child components inside this set are not destroyed automatically. Usually it can be done by corresponding mapper or set destruction in unrender method or via own.
(value: string | AbstractTemplate | HTMLElement | JQuery): this
Selects component rendering strategy. This method is needed only in very rare cases. By default, component is rendered outside of DOM based on template property. You can change this by passing one of the next values into using method of the component:
Note: We strongly encourage you to use standard rendering strategy via template, or at least create HtmlTemplate instances to store your HTML templates. They work 3 times faster compared to raw HTML rendering thanks to preliminary compilation and node cloning method.
protected beforeRender()
Component life stage method. Called during component rendering after HTML template parsing and initialization of references to all elements of the template. Called before render<ChildId> methods and afterRender method. It is convenient to perform some preliminary action here before child component creation. But you are already able to create child components here. Call super.beforeRender() at the first line of the method.
protected afterRender()
Component life stage method. Called after beforeRender method and render<ChildId> methods. You should assign all elements' attributes here, create child components, bind to events and fill component with interactivity. Call super.afterRender() at the first line of the method.
protected afterAppend()
Component life stage method. Called after first-time component appearing in HTML DOM and UI components tree. Component layouting should be performed here (element size computing). Component rendering is finished here. Call super.afterAppend() at the first line of the method.
Caution: the method is called in the next cases only:
In other words, afterAppend method works as expected as long as you render child components as specified in Child components topic.
protected releaseDom()
Component life stage method. Called during component destruction before unrender method call. Everything that was performed in afterAppend method should be reverted here. Call super.releaseDom() at the last line of the method.
protected unrender()
Component life stage method. Called during component destruction before afterDestroy method call. Everything that was performed during component rendering should be reverted here. All child components are already removed by the component before this method call, but the components themselves are not destroyed. You must destroy them explicitly as specified in Component removal and destruction topic. Call super.unrender() at the last line of the method.
protected afterDestroy()
Component life stage method. Called during component destruction after unrender method call. Everything that was performed during component construction should be reverted here. Call super.afterDestroy() at the last line of the method.
protected createElement(): TemplateOutput
Virtual method to render the component document fragment. By default, renders by template.
(): void
Class destructor invocation method. Destroys all aggregated objects and calls destroyObject method. You must call this method explicitly from outside, because JavaScript doesn't support automatic class destructor calling.
const object = new MyClass(); // ... // When the object is not needed anymore, destroy it. object.destroy();
Alternatively (and optimally), you should use own method to aggregate this object inside another one.
protected (): void
Class destructor implementation. Called in destroy method after destruction of all aggregated objects.
The logic of class instance destruction should usually be implemented here. If you override this method,
remember to call super.destroyObject()
at the end of the method:
destroyObject() { // Release resources ... // Call superclass destructor super.destroyObject(); }
<T extends Destroyable>(obj: T): T
Aggregates the object. It means that the specified object is automatically destroyed
on this
object destruction. The aggregated objects are destroyed in reverse order.
Returns obj object, which makes it easy to use in field definition:
private selected = this.own(new Property(false));
(obj: Destroyable): this
The same as own, but returns this
, which makes it easy to use in object instantiation:
const items = new BindableArray().ownValues(); return new Panel(items).owning(items);