jwidget/Component

Consumption

import Component from "jwidget/Component";

Default export#

Hierarchy#

Description#

Base class of UI component. Supports the next features:

  • Rendering by HTML template.
  • Direct access to component elements.
  • jQuery interface for element manipulations.
  • Convenient API for data binding and child component management.

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.

jWidget UI component example#
@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 creation in code#

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");
});
Child components#

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

More about child component collections#

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

More about renderChildId methods#

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
el
Element with corresponding jwid.

Depending on the returned value of this method, you have the next options:

  • If the method returns an instance of Component, then it gets added into children map and becomes a child component. This option doesn't work for the root element.
  • If the method returns an instance of Bindable<Component>, then it gets added as a bindable child component via addBindable method. This option doesn't work for the root element.
  • If the method returns an instance of ReadonlyBindableArray<Component>, then it gets added as a child array via method addArray.
  • If the method returns an instance of ReadonlyBindableSet<Component>, then it gets added as a child collection via method addSet.
  • If the method returns false, then the element gets removed from the HTML document. This option doesn't work for the root element.
  • In any other case, the component doesn't perform any additional actions with the element. You can use el argument to do arbitrary manipulations over it.

The method should be protected, not private, to avoid "unused method" warning of TypeScript compiler.

Component removal and destruction#

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}));
}
Common practices in child component management#
Create a child component

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());
    }
}
Create a bindable child component

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}));
    }
}
Create a child component collection

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));
    }
}
Add existing components as children

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;
    }
}
Component life stages#

Each component has several stages of life.

  1. Like in all other classes, constructor is called first. Usually all fields are assigned to their initial values here, messages are created etc. Only component model should be touched here, view is completely ignored. Notice that component is not rendered after construction yet, so it doesn't have fields el and children assigned, and methods addArray, addSet, addBindable won't work. The main reason behind that is our wish to give you a possibility to do something else between component construction and rendering, for example, change some field values and call some methods. Second reason: it is not recommended to call virtual methods in constructor in any object-oriented language. It may result in undesired side effects. You can render the component directly by calling render, renderTo, renderAs, or by adding this component into another component as a child. For example, component gets rendered immediately once it gets added to children map. You can invoke component rendering multiple times, but it gets rendered only once.
  2. Component rendering flow:
    1. Method createElement is called to create the HTML elements by the template.
    2. The links to all elements are assigned.
    3. Method beforeRender is called. It is convenient to perform some preliminary actions here. You are already able to create child components. Call super.beforeRender() at the first line of the method.
    4. All render<ChildId> methods are called for HTML template elements. The methods are called in the same order as these jwid's are written in the template.
    5. Method afterRender is called at the end of rendering procedure. You should assign all elements' attributes here, create child components, bind event handlers and fill the component with interactivity. Component rendering is finished here. Call super.afterRender() at the first line of the method.
  3. Method afterAppend is called once the component first time appears in HTML DOM and UI component tree. Component layouting should be performed here (i.e. element size computation). Call super.afterAppend() at the first line of the method.
  4. Component destruction flow:
    1. If the component was in DOM, releaseDom method is called. Everything that was performed in afterAppend method, i.e. on step 3, should be reverted here. Call super.releaseDom() at the last line of the method.
    2. The element is removed from DOM. All child components are removed. All child arrays, sets and properties are unbound.
    3. If the component was rendered, unrender method is called. Everything that was performed during component rendering, i.e. on step 2, should be reverted here. You must destroy the child components explicitly here unless you use own method to aggregate them. Call super.unrender() at the last line of the method.
    4. The element is destroyed, i.e. all event handlers and data assigned to the element is released.
    5. Method afterDestroy is called. Nearly everything that was performed in component constructor, i.e. on step 1, should be reverted here. Call super.afterDestroy() at the last line of the method.
    6. All aggregated objects (including child components) are destroyed.
    7. Method destroyObject is called. Call super.destroyObject() at the last line of the method.
    It implies that you should never override destroy method for a Component.
Integration with WebPack#

There's an easy way to attach HTML templates via WebPack. The first example from this topic can be splitted into two files:

MyComponent.ts
@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);
    }
}
MyComponent.jw.html
<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} }
    ]
}

Constructor#

new Component()

Constructs a new instance of Component.

Fields#

parent#

readonly parent: Component

Parent component. The field is available from component rendering beginning.

el#

readonly el: JQuery

Root element. The field is available from component rendering beginning.

children#

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.

template#

readonly template: AbstractTemplate

Component template. Template is defined by template annotation.

Methods#

render#

(): this

returns
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:

  • One of methods renderTo, renderAs is called.
  • The component is added into another component as a child.

Feel free to call component rendering multiple times: it gets rendered only once.

renderTo#

(el: string | HTMLElement | JQuery): this

el
Element to render component into.
returns
this

Renders the component into an element. Use it to render root component only: children must be rendered using children, addArray, addSet, addBindable members.

renderAs#

(el: string | HTMLElement | JQuery): this

el
Element to render component in place of.
returns
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.

remove#

(): this

returns
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.

getElement#

(id: string): JQuery

id
jwid of the element.
returns
Element.

Returns element by its jwid.

removeElement#

(id: string): this

id
jwid of the element.
returns
this

Removes element by jwid. Element gets removed from DOM and destroyed. It is then impossible to get it by getElement method.

addBindable#

(component: Bindable<Component>, id: string): Destroyable

component
Child component property.
id
jwid of element to replace.
returns
Binding.

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.

addArray#

(source: ReadonlyBindableArray<Component>, el?: string | HTMLElement | JQuery): Destroyable

source
Child component array.
el
jwid of element to add child components into. Defaults to root element (el) of component.
returns
Binding.

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.

addSet#

(source: ReadonlyBindableSet<Component>, el?: string | HTMLElement | JQuery): Destroyable

source
Child component collection.
el
jwid of element to add child components into. Defaults to root element (el) of component.
returns
Binding.

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.

using#

(value: string | AbstractTemplate | HTMLElement | JQuery): this

value
Template or element to use for component rendering.
returns
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:

  • AbstractTemplate or string - use this template explicitly for rendering.
  • HTMLElement or JQuery - build component on top of existing DOM element. Special attributes jwclass and jwid get processed in the usual way.

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.

beforeRender#

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.

afterRender#

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.

afterAppend#

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:

  • renderTo or renderAs method is called, and the parent element is present in the document body.
  • The component is added as a child component to a component that has afterAppend method called in the past.
  • This is a child component of a component that has afterAppend called at the moment.
  • render method is called, and using method was called with an HTMLElement or jQuery element passed.

In other words, afterAppend method works as expected as long as you render child components as specified in Child components topic.

releaseDom#

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.

unrender#

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.

afterDestroy#

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.

createElement#

protected createElement(): TemplateOutput

returns
HTML template rendering output.

Virtual method to render the component document fragment. By default, renders by template.

destroy (inherited from Class)#

(): 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.

destroyObject (inherited from Class)#

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();
}
own (inherited from IClass)#

<T extends Destroyable>(obj: T): T

obj
Object to aggregate.
returns
obj

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));
owning (inherited from IClass)#

(obj: Destroyable): this

obj
Object to aggregate.
returns
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);