Source: https://github.com/enepomnyaschih/mt/tree/mt-2.1-3 (Git branch).
In this part we learn how to render child components that don't belong to lists. Examples of such components are user profile panel and tweet feed - they are child components of application.
First, let's define ApplicationData class - new root model of the application.
import Tweet, {createTweetByJson} from "./Tweet"; export default class ApplicationData { constructor(readonly tweets: Tweet[] = []) { } static createByJson(json: any) { return new ApplicationData((<any[]>json || []).map(createTweetByJson)); } }
Now let's implement Application class - new root view of the application.
<div jwclass="mt-application"> <div jwid="wrap"> <div jwid="profile-box"></div> <div jwid="tweets"></div> </div> </div>
import Component from "jwidget/Component"; import template from "jwidget/template"; import ApplicationData from "../model/ApplicationData"; import TweetFeed from "./TweetFeed"; @template(require<string>("./Application.jw.html")) export default class Application extends Component { constructor(private data: ApplicationData) { super(); } protected renderTweets() { return this.own(new TweetFeed(this.data.tweets)); } protected afterRender() { super.afterRender(); $("html").addClass("mt-html"); $("body").addClass("mt-body"); } }
Once again, we see method render<ChildId> (renderTweets), and once again it does something new. This time, it creates and returns an instance of TweetFeed class. It means that the instance of TweetFeed will be rendered at the place of element with jwid="tweets". This element will be completely replaced with the component, and all its CSS classes (here: "mt-application-tweets") will be copied into root element of the component.
Let's review some details of this feature.
First, it is important to understand that after instantiation of TweetFeed class object the component is not rendered yet. It means that it doesn't have HTML elements and capability to add child components. Component will be rendered automatically somewhere inside the framework, later. But if you really need to perform some additional actions with the rendered component (for example, add a CSS-class), you can render it explicitly using render method.
protected renderTweets() { const view = this.own(new TweetFeed(this.data.tweets)); view.render(); view.el.addClass("my-extra-class"); return view; }
Second, like in the previous parts, we should have a way to add a child component without render<ChildId> method definition. You can do it using children map.
protected afterRender() { super.afterRender(); this.children.put("tweets", this.own(new TweetFeed(this.data.tweets))); }
Select the way you like more, but, again, I will stick to render<ChildId> method.
Add styles.
.blocklink color inherit display block font inherit font-size 100% text-decoration inherit .mt-html .mt-body background #c0deed .mt-application font-family Arial, sans-serif &-wrap background rgba(255,255,255,0.5) box-sizing border-box margin 0 auto overflow hidden padding 15px width 868px &-profile-box float left width 302px &-tweets float left margin-left 13px
Update index.styl and index.ts to apply the changes.
// All Stylus files should be imported here in the preferred order @import "view/Application" @import "view/TweetFeed" @import "view/TweetView"
import "es6-promise/auto"; import "script-loader!jquery"; import "./index.styl"; import ApplicationData from "./model/ApplicationData"; import Application from "./view/Application"; $(function () { const data = ApplicationData.createByJson([ { "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 } ]); new Application(data).renderTo("body"); });
We'll see the next result.
Profile panel is remaining to implement.
Let's start with model once again. We need to collect data about current user profile. Let's create Profile model.
export default interface Profile { readonly fullName: string; readonly shortName: string; readonly avatarUrl32: string; readonly avatarUrl48: string; readonly tweets: number; readonly following: number; readonly followers: number; }
Now, let's add Profile to ApplicationData.
import Profile from "./Profile"; import Tweet, {createTweetByJson} from "./Tweet"; export default class ApplicationData { constructor(readonly profile: Profile, readonly tweets: Tweet[] = []) { } static createByJson(json: any) { return new ApplicationData(json.profile, (<any[]>json.tweets || []).map(createTweetByJson)); } }
Let's switch to view. Implement ProfileBox.
<div jwclass="mt-profile-box"> <a jwid="top" class="blocklink" href="#"> <div jwid="avatar"></div> <div jwid="full-name"></div> <div jwid="show-profile">Show my profile</div> </a> <div jwid="middle"> <a jwid="count tweets" class="blocklink" href="#"> <div jwid="count-value tweets-value"></div> <div jwid="count-label">TWEETS</div> </a> <a jwid="count count-border following" class="blocklink" href="#"> <div jwid="count-value following-value"></div> <div jwid="count-label">FOLLOWING</div> </a> <a jwid="count count-border followers" class="blocklink" href="#"> <div jwid="count-value followers-value"></div> <div jwid="count-label">FOLLOWERS</div> </a> </div> <div jwid="bottom"> <form jwid="compose-form"> <div jwid="compose-fields"> <textarea jwid="compose-input" placeholder="Compose tweet..."></textarea> </div> <div jwid="compose-buttons"> <input jwid="compose-submit" type="submit" value="Tweet"> </div> </form> </div> </div>
import Component from "jwidget/Component"; import template from "jwidget/template"; import Profile from "../model/Profile"; @template(require<string>("./ProfileBox.jw.html")) export default class ProfileBox extends Component { constructor(private profile: Profile) { super(); } protected renderTop(el: JQuery) { el.attr("href", "https://twitter.com/" + this.profile.shortName); } protected renderAvatar(el: JQuery) { el.css("background-image", "url(" + this.profile.avatarUrl32 + ")"); } protected renderFullName(el: JQuery) { el.text(this.profile.fullName); } protected renderTweets(el: JQuery) { el.attr("href", "https://twitter.com/" + this.profile.shortName); } protected renderTweetsValue(el: JQuery) { el.text(this.profile.tweets); } protected renderFollowingValue(el: JQuery) { el.text(this.profile.following); } protected renderFollowersValue(el: JQuery) { el.text(this.profile.followers); } }
.mt-profile-box background #f9f9f9 border 1px solid rgba(0,0,0,0.45) border-radius 6px box-sizing border-box &-full-name &-count-value &-compose-submit color #333 font-family Arial, sans-serif font-size 14px font-weight bold text-shadow 0 1px 0 #fff &-show-profile, &-count-label color #999 font-family Arial, sans-serif font-size 11px text-shadow 0 1px 0 #fff &-top border-bottom 1px solid #e8e8e8 overflow hidden padding 12px padding-bottom 2px &-avatar background transparent none no-repeat 0 0 border-radius 3px float left margin 0 10px 10px 0 width 32px height 32px &-full-name padding-top 2px &-top:hover &-full-name color #0084b4 text-decoration underline &-middle overflow hidden &-count float left padding 7px 12px &:hover &-value color #0084b4 &:hover &-label color #0084b4 &-border border-left 1px solid #e8e8e8 &-bottom background #f5f5f5 border-radius 0 0 6px 6px border-top 1px solid #e8e8e8 padding 10px 12px &-compose &-input border 1px solid #ccc border-radius 3px box-sizing border-box padding 8px width 274px &-buttons text-align right &-submit background #19aadf border 1px solid #057ed0 border-radius 4px color #fff cursor pointer padding 6px 10px text-shadow 0 -1px 0 rgba(0,0,0,0.45) &:hover background #09a0d7
Add profile box rendering method to Application.
protected renderProfileBox() { return this.own(new ProfileBox(this.data.profile)); }
Register ProfileBox styles.
// All Stylus files should be imported here in the preferred order @import "view/Application" @import "view/ProfileBox" @import "view/TweetFeed" @import "view/TweetView"
And enhance the data JSON with profile data.
import "es6-promise/auto"; import "script-loader!jquery"; import "./index.styl"; import ApplicationData from "./model/ApplicationData"; import Application from "./view/Application"; $(function () { const data = ApplicationData.createByJson({ "profile": { "fullName": "Road Runner", "shortName": "roadrunner", "avatarUrl32": "backend/avatar-32.png", "avatarUrl48": "backend/avatar-48.png", "tweets": 380, "following": 21, "followers": 27 }, "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 } ] }); new Application(data).renderTo("body"); });
Here is the result, which represents original requirements.
We've learned how to render components and add them into each other. Now it's the time to add a bit of dynamics into our application. We'll describe this in next part.
Tutorial. Part 4. Events