Tutorial. Part 2. Child component collections

Source: https://github.com/enepomnyaschih/mt/tree/mt-2.1-2 (Git branch).

In this part we meet List class. We will learn how to use it to display child UI component lists. Our goal is to render a list of tweets developed in the previous part.

Let's dive directly to the view. Define class TweetFeed.

src/view/TweetFeed.jw.html
<div jwclass="mt-tweet-feed">
    <div jwid="header">Tweets</div>
    <div jwid="tweets"></div>
    <div jwid="footer">...</div>
</div>
src/view/TweetFeed.ts
import Component from "jwidget/Component";
import List from "jwidget/List";
import template from "jwidget/template";
import Tweet from "../model/Tweet";
import TweetView from "./TweetView";

@template(require<string>("./TweetFeed.jw.html"))
export default class TweetFeed extends Component {

    constructor(private tweets: Tweet[]) {
        super();
    }

    protected renderTweets() {
        const tweetViews = this.tweets.map(tweet => new TweetView(tweet));
        return this.own(new List(tweetViews)).ownItems();
    }
}

Let's review renderTweets method in details. Similarly to TweetView component, we've defined method render<ChildId> for element with jwid="tweets". But now this method not just fills the element with data, but renders a list of child components into it. This list is created from an array of tweet models with the following steps:

  1. Models get mapped to views via ES5 map method of array.
  2. View array gets converted to jWidget List.
  3. The List gets aggregated in TweetFeed via own method, and its items get aggregated in it with ownItems method.

jWidget components recognize jWidget lists of components, not arrays. If you return such a list as a result of render<ChildId> method, then this list gets rendered into the corresponding element as a list of child components.

Aggregation methods own and ownItems control the life time of child components. If object A owns object B, then destruction of object A automatically triggers destruction of object B. In our case, destruction of TweetFeed automatically triggers destruction of tweet view list. Thanks to ownItems call, destruction of the list triggers destruction all its elements, i.e. tweet views. It is a good practice to destroy child UI components when you don't need them anymore, because any UI component may initialize its own bindings you can be unaware of. See Common practices in child component management for more instructions about how this can be achieved.

Let's define styles.

src/view/TweetFeed.styl
.mt-tweet-feed
    background #fff
    border 1px solid rgba(0,0,0,0.45)
    border-radius 6px
    box-sizing border-box
    width 522px

    &-header
        color #333
        font-family Arial, sans-serif
        font-size 18px
        font-weight bold
        padding 10px
        text-shadow 0 1px 0 #fff

    &-footer
        border-top 1px solid #e8e8e8
        padding 8px
        text-align center

Add the file to index.styl:

// All Stylus files should be imported here in the preferred order

@import "view/TweetFeed"
@import "view/TweetView"

And prepare new test data.

src/index.ts
import "es6-promise/auto";
import "script-loader!jquery";
import "./index.styl";

import {createTweetByJson} from "./model/Tweet";
import TweetFeed from "./view/TweetFeed";

$(function () {
    const tweets = [
        {
            "fullName": "Road Runner",
            "shortName": "roadrunner",
            "avatarUrl48": "backend/avatar-48.png",
            "contentHtml": "jWidget documentation is here <a href=\"https://enepomnyaschih.github.com/jwidget\" target=\"_blank\">enepomnyaschih.github.com/jwidget</a>",
            "timeAgo": 215000,
            "like": false,
            "retweet": true
        },
        {
            "fullName": "Road Runner",
            "shortName": "roadrunner",
            "avatarUrl48": "backend/avatar-48.png",
            "contentHtml": "Tweet feed is growing",
            "timeAgo": 515000,
            "like": false,
            "retweet": false
        }
    ].map(createTweetByJson);
    new TweetFeed(tweets).renderTo("body");
});

Running the application in the browser displays the expected result.

Let's review one more way of child component rendering, without render<ChildId> method definition. Let's remove renderTweets method and override afterRender method instead:

import Component from "jwidget/Component";
import List from "jwidget/List";
import template from "jwidget/template";
import Tweet from "../model/Tweet";
import TweetView from "./TweetView";

@template(require<string>("./TweetFeed.jw.html"))
export default class TweetFeed extends Component {

    constructor(private tweets: Tweet[]) {
        super();
    }

    protected afterRender() {
        super.afterRender();
        const tweetViews = this.tweets.map(tweet => new TweetView(tweet));
        const tweetViewList = this.own(new List(tweetViews)).ownItems();
        this.addList(tweetViewList, "tweets");
    }
}

This code is equivalent to the original one, but child component list is added dynamically with addList method. This method takes element "jwid" as second argument, which should be used as a container for child components passed in the first argument. If we won't pass second argument, the array will be rendered into root element. Use the way you like more. I'll stick to the first way, utilizing render<ChildId> method.

Tutorial. Part 3. Named child components