Source: https://github.com/enepomnyaschih/mt/tree/mt-2.1-1 (Git branch).
This tutorial shows you a way to develop your own Twitter in several steps using jWidget 2 and TypeScript.
If you want to learn how to develop powerful Model-View-applications with jWidget 2, please follow these steps accurately in the same order. If motivation of some action is unclear for you, then probably we'll explain it at the next steps. Please don't deviate from this strategy yet.
In the first sample we take a look at model and view. We will develop a simple UI component which takes data from a model to render itself.
First, download jWidget 2 project template. The template gives you a good starting point. Now you don't need to bother about the environment configuration process. Just extract the template to some folder at your disk and run:
npm install npm start
These commands install the dependencies, create "public" folder with the compiled application files and start watching the source files for any modifications. You need to serve "public" folder via some Web server, for example (run in a separate terminal):
npm install -g serve serve public
This command will display the URL of the served files. Open it in your browser. It should display "Hello, World!" message.
Now we are ready to depelop the application. Download profile avatar files avatar-32.png and avatar-48.png to "static/backend" folder.
Our goal is to implement a component for tweet representation:
Let's find out, what data we have here, i.e. what can be different for a couple of different tweets:
Let's define an interface storing this data, i.e. tweet model:
export default interface Tweet {
readonly fullName: string;
readonly shortName: string;
readonly avatarUrl48: string;
readonly contentHtml: string;
readonly time: number;
readonly like: boolean;
readonly retweet: boolean;
}
export function createTweetByJson(json: any): Tweet {
return {
...json,
time: new Date().getTime() - json["timeAgo"]
};
}
Model development is finished, so let's implement a view. Define TweetView component as follows:
import Component from "jwidget/Component"; import template from "jwidget/template"; import Tweet from "../model/Tweet"; @template(require<string>("./TweetView.jw.html")) export default class TweetView extends Component { constructor(_tweet: Tweet) { super(); } }
Next, we need to bind an HTML template to this component. It should be defined in a separate file:
<div jwclass="mt-tweet">
<div jwid="avatar"></div>
<div jwid="content">
<div jwid="header">
<div jwid="full-name"></div>
<div jwid="short-name"></div>
<div jwid="time"></div>
</div>
<div jwid="text"></div>
<div jwid="buttons">
<a jwid="button like" href="#"></a>
<a jwid="button retweet" href="#"></a>
<a jwid="button remove" href="#">Remove</a>
</div>
</div>
</div>
This is usual HTML except one thing: special attributes "jwclass" and "jwid". "jwclass" is a root CSS-class of the component, and the prefix for all elements which have "jwid" defined. CSS-class of each element with "jwid" will be <jwclass>-<jwid>. So, the template above will expand to the next HTML:
<div class="mt-tweet">
<div class="mt-tweet-avatar"></div>
<div class="mt-tweet-content">
<div class="mt-tweet-header">
<div class="mt-tweet-full-name"></div>
<div class="mt-tweet-short-name"></div>
<div class="mt-tweet-time"></div>
</div>
<div class="mt-tweet-text"></div>
<div class="mt-tweet-buttons">
<a class="mt-tweet-button mt-tweet-like" href="#"></a>
<a class="mt-tweet-button mt-tweet-retweet" href="#"></a>
<a class="mt-tweet-button mt-tweet-remove" href="#">Remove</a>
</div>
</div>
</div>
Presence of a common prefix mt-tweet- in all elements simplifies component slicing via various CSS-preprocessors like Sass, LESS and Stylus.
It is time to run our application. To do it, we need testing data and a main entry point. Let's define them in index.ts file.
import "es6-promise/auto";
import "script-loader!jquery";
import "./index.styl";
import {createTweetByJson} from "./model/Tweet";
import TweetView from "./view/TweetView";
$(function () {
const tweet = createTweetByJson({
"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
});
new TweetView(tweet).renderTo("body");
});
The first two imports are mandatory for jWidget, because the framework depends on native ES6 promises and jQuery. Native ES6 promises are not yet supported by all browsers, so we need to register a polyfill for it.
Make sure that WebPack (running under "npm start") doesn't display any errors and refresh the browser page. You'll see something like this:
As you can see, our component has a structure but doesn't have any data. Let's bind the component elements to the data. jWidget framework doesn't provide any magic HTML syntax for this. So, we won't change the template, but we'll add some TypeScript code. The real tool that jWidget provides for us is direct and fast access to jQuery-wrappers over all HTML elements having "jwid" attribute defined. You can access these elements inside method afterRender via method getElement:
import Component from "jwidget/Component"; import template from "jwidget/template"; import Tweet from "../model/Tweet"; @template(require<string>("./TweetView.jw.html")) export default class TweetView extends Component { constructor(private tweet: Tweet) { super(); } protected afterRender() { super.afterRender(); this.getElement("avatar").css("background-image", `url(${this.tweet.avatarUrl48})`); const timeAgo = new Date().getTime() - this.tweet.time; const text = this._getTimeString(timeAgo); this.getElement("time").text(text); this.getElement("full-name").text(this.tweet.fullName); this.getElement("short-name").text("@" + this.tweet.shortName); this.getElement("text").html(this.tweet.contentHtml); this.getElement("like").toggleClass("active", this.tweet.like).text(this.tweet.like ? "Unlike" : "Like"); this.getElement("retweet").toggleClass("active", this.tweet.retweet).text(this.tweet.retweet ? "Unretweet" : "Retweet"); } private _getTimeString(timeAgo: number) { const minutes = timeAgo / 60000; if (minutes < 1) { return "Just now"; } if (minutes < 60) { return Math.floor(minutes) + "m"; } const hours = minutes / 60; if (hours < 24) { return Math.round(hours) + "h"; } function pad(value: number): string { return (value < 10) ? ("0" + value) : String(value); } const date = new Date(new Date().getTime() - timeAgo); return date.getDate() + "." + pad(date.getMonth()); } }
Result:
Works well, but looks poorly. Let's demonstrate the magic of slicing. I prefer Stylus for this purpose, and its interpreter is already configured in jWidget project template.
.mt-tweet
background #fff
border-top 1px solid #e8e8e8
box-sizing border-box
font-family Arial,sans-serif
font-size 14px
overflow hidden
padding 12px
width 520px
&:hover
background #f5f5f5
&-full-name
color #333
font-family Arial, sans-serif
font-size 14px
font-weight bold
text-shadow 0 1px 0 #fff
&-short-name
&-time
color #999
font-family Arial, sans-serif
font-size 11px
text-shadow 0 1px 0 #fff
&-avatar
background transparent none no-repeat 0 0
border-radius 5px
float left
margin-right 10px
width 48px
height 48px
&-content
float left
width 438px
&-header
overflow hidden
&-full-name
float left
margin-right 4px
&-short-name
float left
&-time
float right
&-text
padding 5px 0
&-buttons
text-align right
&-button
color #0084b4
cursor pointer
display inline-block
&-like
&-retweet
margin-right 10px
&-like.active
color #ff9b00
&-retweet.active
color #609928
Add this Stylus file into index.styl:
// All Stylus files should be imported here in the preferred order @import "view/TweetView"
As result, we'll see what we wanted to:
Let's review one more thing. We can write JS code of the component in a different way. Instead of accessing the elements via getElement method, let's just define methods render<ChildId>, where <ChildId> is "jwid" of an element written in CapitalizedCamelCase:
import Component from "jwidget/Component"; import template from "jwidget/template"; import Tweet from "../model/Tweet"; @template(require<string>("./TweetView.jw.html")) export default class TweetView extends Component { constructor(private tweet: Tweet) { super(); } protected renderAvatar(el: JQuery) { el.css("background-image", `url(${this.tweet.avatarUrl48})`); } protected renderTime(el: JQuery) { const timeAgo = new Date().getTime() - this.tweet.time; const text = this._getTimeString(timeAgo); el.text(text); } protected renderFullName(el: JQuery) { el.text(this.tweet.fullName); } protected renderShortName(el: JQuery) { el.text("@" + this.tweet.shortName); } protected renderText(el: JQuery) { el.html(this.tweet.contentHtml); } protected renderLike(el: JQuery) { el.toggleClass("active", this.tweet.like).text(this.tweet.like ? "Unlike" : "Like"); } protected renderRetweet(el: JQuery) { el.toggleClass("active", this.tweet.retweet).text(this.tweet.retweet ? "Unretweet" : "Retweet"); } private _getTimeString(timeAgo: number) { const minutes = timeAgo / 60000; if (minutes < 1) { return "Just now"; } if (minutes < 60) { return Math.floor(minutes) + "m"; } const hours = minutes / 60; if (hours < 24) { return Math.round(hours) + "h"; } function pad(value: number): string { return (value < 10) ? ("0" + value) : String(value); } const date = new Date(new Date().getTime() - timeAgo); return date.getDate() + "." + pad(date.getMonth()); } }
This code is equivalent to the original one. There is slightly more code, but it became more readable. Each specific element is rendered with its own method. You can use one way or another. I prefer second way because it is more flexible: you can override any element rendering in an inherited component class. Let's stick to this way in future samples.
Tutorial. Part 2. Child component collections