Source: https://github.com/enepomnyaschih/mt/tree/mt-2.3-4 (Git branch).
In this part we learn how to handle user events in scope of a jWidget application. First, we must activate buttons Like/Unlike and Retweet/Unretweet.
Here's a common scenario of Like button click handling in model-view architecture.
Notice that the view can update itself directly on its own before step 2. But it is unneccessary in model-view architecture, because the view listens "onLikeChange" anyway. Who knows what may dispatch model modification? Any other views or services may access the model directly, but they hardly can access the corresponding views. That's why the view should get updated indirectly. It should listen to model modifications and handle them appropriately. This way, it can keep itself updated.
In some other model-view frameworks (actually, MVC frameworks) controller is responsible for event listening and handling. jWidget doesn't provide any features for controller implementation. Controller part is identified by the author of the library as unnecessary and complicating things with no benefit. jWidget goes without controller.
Message management API is optimized for performance in jWidget and implemented according to OOD principles. That's the reason why jWidget message API is very different from jQuery event API. jWidget messages get dispatched from Dispatcher class and defined by its interfaces Listenable and IDispatcher. One dispatcher instance represents one kind of action. In contrast, jQuery element is an object that manages events in jQuery and it represents all kinds of events that may happen to this element, and the event kind is identified by its name. jQuery implementation generates redundancy, because all events have a common set of parameters regardless of event kind difference. For example, it makes no sense to read mouse coordinates from key press event, but jQuery type definitions provide you with them anyway. In jWidget, you can specify different message types for different kinds of dispatchers, and that is easier to manage.
So, let's start listening Like and Retweet button clicks. Let's follow the steps described above. First, bind handlers to click events using jQuery in TweetView class:
protected renderLike(el: JQuery) { el.toggleClass("active", this.tweet.like).text(this.tweet.like ? "Unlike" : "Like"); el.on("click", event => { event.preventDefault(); this.tweet.like = !this.tweet.like; }); } protected renderRetweet(el: JQuery) { el.toggleClass("active", this.tweet.retweet).text(this.tweet.retweet ? "Unretweet" : "Retweet"); el.on("click", event => { event.preventDefault(); this.tweet.retweet = !this.tweet.retweet; }); }
At the next step, we should replace Tweet interface with a class. Readonly fields "like" and "retweet" should be replaced with getters and setters. To implement them, we need to define onLikeChange and onRetweetChange. Below you can see a lot of boilerplate code to make it work, but don't worry about it - we'll deal with this problem later.
import Dispatcher from "jwidget/Dispatcher"; import Listenable from "jwidget/Listenable"; export default class Tweet { readonly fullName: string; readonly shortName: string; readonly avatarUrl48: string; readonly contentHtml: string; readonly time: number; private _like: boolean; private _retweet: boolean; private _onLikeChange = new Dispatcher<boolean>(); private _onRetweetChange = new Dispatcher<boolean>(); constructor(config: TweetConfig) { this.fullName = config.fullName; this.shortName = config.shortName; this.avatarUrl48 = config.avatarUrl48; this.contentHtml = config.contentHtml; this.time = config.time; this._like = config.like; this._retweet = config.retweet; } get like() { return this._like; } set like(value) { if (this._like !== value) { this._like = value; this._onLikeChange.dispatch(value); } } get onLikeChange(): Listenable<boolean> { return this._onLikeChange; } get retweet() { return this._retweet; } set retweet(value) { if (this._retweet !== value) { this._retweet = value; this._onRetweetChange.dispatch(value); } } get onRetweetChange(): Listenable<boolean> { return this._onRetweetChange; } static createByJson(json: any) { return new Tweet({ ...json, time: new Date().getTime() - json["timeAgo"] }); } } export interface TweetConfig { readonly fullName: string; readonly shortName: string; readonly avatarUrl48: string; readonly contentHtml: string; readonly time: number; readonly like: boolean; readonly retweet: boolean; }
A message is dispatched with dispatch method. The message is passed to all listeners. In this case the message is boolean - the new value of the property. jWidget Dispatcher is very simple: all listeners get iterated and called one by one synchronously right in dispatch method. There are no special features like enqueueing, bubbling, "preventDefault" or "stopPropagation", as well as there is no pretection against dead loops or circular dependencies. If you want to introduce something like this, implement it on your own using IDispatcher interface. jWidget is modest in this regard, but fast.
Since we replaced "createTweetByJson" function with "createByJson" static method, let's immediately change its usage.
static createByJson(json: any) { return new ApplicationData(json.profile, (<any[]>json.tweets || []).map(Tweet.createByJson)); }
Next, we must bind handlers to the messages and update the view in them. In contrast to React or Backbone, you don't need to update the entire view in jWidget - you just update what you need. To prevent code duplication, let's extract element updating code of TweetView into separate methods "updateLike" and "updateRetweet".
protected renderLike(el: JQuery) { this._updateLike(); this.tweet.onLikeChange.listen(() => this._updateLike()); el.on("click", event => { event.preventDefault(); this.tweet.like = !this.tweet.like; }); } protected renderRetweet(el: JQuery) { this._updateRetweet(); this.tweet.onRetweetChange.listen(() => this._updateRetweet()); el.on("click", event => { event.preventDefault(); this.tweet.retweet = !this.tweet.retweet; }); } private _updateLike() { this.getElement("like").toggleClass( "active", this.tweet.like).text(this.tweet.like ? "Unlike" : "Like"); } private _updateRetweet() { this.getElement("retweet").toggleClass( "active", this.tweet.retweet).text(this.tweet.retweet ? "Unretweet" : "Retweet"); }
It must work! Try to run it in a browser and click Like/Unlike and Retweet/Unretweet buttons. Moreover, you can feel the transparency of this approach even more. Just for fun, define a global hook to access the model.
(<any>window).data = data;
Refresh the page, open the browser console and run the next command:
data.tweets[0].like = true;
Your application will obediently reflect the change.
You may find this code too bulky. We have written a ton of code just to implement a couple of very simple event handlers. Your judgement is absolutely justified. The reason behind that is our desire to help you understanding the spirit of the model-view architecture. That's just how it works on low level. And that's what will make your life easier if you decide to add more views to the models you already have. However, you indeed don't need so much code to maintain proper architecture - jWidget provides a bunch of useful utilities that will reduce this code drastically, and we will demonstrate them in the next tutorial.
Tutorial. Part 5. Properties