Presenter First (MVP) using TypeScript (Javascript)
When we start writing GUI application specifically with javascript, it become very easily unorganised and tightly coupled with the screen widgets. And its very hard to understand the real customer requirement(. business logic). But using Model-View-Presenter design pattern, we can easily decouple the UI(where customer interact), presentation logic and business logic. As the name suggest presenter first, so we start thinking with the presenter, then we think about the interaction of user intent events with the model and so.
MVP improves the separation of concerns in presentation logic:
- The model: where we defines the data to be displayed or where data should be kept to manipulate.
- The view: is an interface that displays data (the model) and routes user commands (events) to the presenter to act upon that data.
- The presenter: acts upon the model and the view. It retrieves data from the model, and formats it for display in the view
So the main aim of MVP is decoupling of different aspects in the code. Basically there is 3 main such aspects in javascript,
- DOM Manipulation
- Event Handling
- Server Communication(Ajax calls etc)
For each of these concerns, there’s a corresponding term from MVP:
- Event Handling = Presenter
- DOM Manipulation = View
- AJAX calls = Model
But there are some rule when defining the MVP, view should always raise events which matters to business logic or more specifically user intent events, It should not directly call the presenter, because our aim is to decouple the view and presenter, right.
Same with model, when data or state updates model should raise an event, which is handled by presenter. So presenter can inform the view to update the DOM.
So only presenter can directly give command to the view and model, as it knows the user intent.
This can be understand by the diagram displayed below:
Now you know about the MVP pattern, let’s implement it using typescript:
Let’s take a simple example of user form submission. And also we want to change the some information before submitting the form. Let’s say we want to ask user first name and last name in different input, but want to send full name to the server.
So here the user intent is to register himself/herself.
We will be using Riotjs which will help us to use observer pattern.
Using Riotjs, we can easily raise an event and subscribe to it. Using it we can also notify the subscriber.
here is the basic skeleton code:
Basic html:
<form action=”user” method=”post” id=”user-form”>
<input name=”first_name”/>
<input name=”last_name”/>
<button type=”submit”> register </button>
</form>
<h3 id=”full-name”></h3>
Typescript code:
module UserRegistration {
declare var $: any;
declare var riot: any;
interface SerializeUserInformation {}
interface RegisterUser {}
export class View implements SerializeUserInformation { }
export class Presenter {
constructor(view: SerializeUserInformation, model: RegisterUser){}
}
export class Model implements RegisterUser {}
}
So we just declare the UserRegistration module which will contain our triad (MVP). Also try to give name to your interfaces according to user intent or actual business logic . So that interface name is clear enough to sense the functionality which implementing class will be doing. Now let’s first display the full name(this is kind of validation, mean we will validate our form in the view layer only) which user insert.
module UserRegistration {
declare var $: any;
declare var riot: any;
interface SerializeUserInformation {
whenUserRegister(callback: () => void): void;
}
interface RegisterUser {}
export class View implements SerializeUserInformation {
fullName: string;
constructor() {
this.registerWithForm();
} whenUserRegister() {} private showFullName() {
this.fullName =
$(“#first-name”).val() + “ “ + $(“#last-name”).val(); $(“#full-name”).html(this.fullName);
} private registerWithForm() {
var _this = this;
$(“#user-form”).submit(function(e) {
e.preventDefault();
_this.showFullName();
});
}
} export class Presenter {
constructor(
view: SerializeUserInformation, model: RegisterUser) {}
}
export class Model implements RegisterUser {}}
Now first we will register our presenter with whenUserRegister and whenUserIsSaved. Because view and model will broad cast events they don’t know how they are handle, but whatever happens they are not responsible(SRP), as it is the responsibility of presenter.
module UserRegistration { declare var $: any; declare var riot: any; interface SerializeUserInformation {
whenUserRegister(callback: (userData: JSON) => void): void;
showWelcomeMessage();
} interface RegisterUser {
saveUser(userData: JSON): void;
whenUserIsSaved(callback: () => void): void;
} export class View implements SerializeUserInformation {
fullName: string;
announcer = riot.observable(this);
constructor(){
this.registerWithForm();
}
whenUserRegister(callback: (userData: JSON) => void) {
console.log("user is registered");
this.announcer.on("whenUserRegister", callback);
} showWelcomeMessage(){
console.log("welcome user");
}
private showFullName() {
this.fullName = $("#first-name").val() + " " + $("#last-name").val();
$("#full-name").html(this.fullName);
} private registerWithForm(){
var _this = this;
$("#user-form").submit(function(e) {
e.preventDefault();
_this.showFullName();
_this.announcer.trigger("whenUserRegister", {fullName:
_this.fullName});
});
}
} export class Presenter {
constructor(
private view: SerializeUserInformation,
private model: RegisterUser) { view.whenUserRegister(this.saveUserCallback());
model.whenUserIsSaved(this.showWelcomeMessage());
} saveUserCallback() {
//call the model the same way
return (userData: JSON) => {
console.log(this);
this.model.saveUser(userData);
console.log("data is transferred");
}
} showWelcomeMessage(){
return () => {
console.log("hey i am called");
this.view.showWelcomeMessage();
}
//call view to render welcome message;
} } export class Model implements RegisterUser {
announcer = riot.observable(this);
constructor(){} saveUser(userData: JSON){
var _this = this;
$.ajax({
type: 'POST',
url: $(this).attr("action"),
data: JSON.stringify(userData),
success: function(data) {
_this.announcer.trigger("whenUserIsSaved");
},
contentType: "application/json",
dataType: 'json'
});
} whenUserIsSaved(callback: () => void){
this.announcer.on("whenUserIsSaved", callback);
}
}
}
You can check the code at this.
So using presenter first approach, we can easily keep our code from becoming polluted and keep it decoupled with first order interactions.