TL;DR: Domain models are important for defining and enforcing business logic in applications and are especially relevant as apps become larger and more people work on them. In this article, we show how this can be done in an Angular 2 app by moving logic out of the component and into a model. We also cover how to inject model classes using Angular 2's new Dependency Injection system. Check out the repo for the article to see the code.
This is the second part of our Angular 2 series. You can also check out the first part about Angular 2 pipes.
Auth0's Angular 2 series brings you tutorials on how to implement the latest features from the framework using the most recent Alpha release at the time of writing.
As applications grow in size, it becomes increasingly important that they be well organized with a solid structure in place. This is especially critical as increasing numbers of developers are added to the team. Application architecture should also strongly focus on the business rules that govern the application and protect sensitive methods from being exposed where they shouldn't be.
“As applications grow in size, it becomes important that they be well organized with a solid structure in place”
Tweet This
At the same time, it is important that we keep our applications DRY and maintainable by moving logic out of components themselves and into separate classes that can be called upon. A modular approach such as this makes our app's business logic reusable.
One way we can achieve this type of structure in Angular 2 applications is by using models to organize our business logic. Whereas the term "model" in Angular has typically been used to refer to the View-Model, what we're discussing here is the domain model-or the set of rules and business logic that an application implements for it to adhere to the organization's needs.
The term "domain model" is, of course, a generic one. Domain models are relevant in any kind of application. In this tutorial, we will see how we can abstract business logic to a model in an Angular 2 app and use the new Dependency Injection (DI) system to call upon these models. We will also explore the DI system's features and various ways it can be used.
Getting Started with an Angular 2 app
We'll be writing our app in TypeScript, but everything will work just fine in ES6. If you'd like an Angular 2 starter app in ES6, checkout Pawel Kozlowski's ng2-play repo.
Our sample app will take a user's details as input and use a model to handle those values. Let's start with a simple component called
UsersAppComponent
.// app.ts /// <reference path="typings/_custom.d.ts" /> import {Component, View, bootstrap, bind, FORM_DIRECTIVES} from 'angular2/angular2'; import {Inject} from 'angular2/di'; @Component({ selector: 'users' }) @View({ // Form directives to be used in the template directives: [FORM_DIRECTIVES], templateUrl: 'userTemplate.html' }) class UsersAppComponent { constructor() {} } bootstrap(UsersAppComponent);
Our template has some simple inputs for the user to provide their information, including checkboxes for them to select which programming languages they know.
<!-- userTemplate.html --> <form role="form" #form="form" (ng-submit)="submit(form.value)"> <!-- We can make use of two-way data binding with ng-model --> <input type="text" placeholder="Enter your name" [(ng-model)]="user.name"> <input type="text" placeholder="Enter your email" [(ng-model)]="user.email"> <h3>Languages</h3> <input type="checkbox" [(ng-model)]="user.javascript">JavaScript <input type="checkbox" [(ng-model)]="user.ruby">Ruby <input type="checkbox" [(ng-model)]="user.php">PHP <button>Submit</button> </form>
Handling User Data
For our app, we'll want to take the user's input to see how it can be handled with a model. For simplicity, we'll just log the info to the console in these examples, but you would typically be doing HTTP requests to send the user data off to a server to be persisted.
We could set up a simple
User
class that acts as a model for our data and have some additional methods in our UsersAppComponent
that act upon the form data. We can do this right within our component, but as will become evident later, there are some drawbacks to this. Let's see what it would look like for now. The class arrangement will be:// app.ts ... class User { name: string; email: string; rating: number; } @Component({ selector: 'users' }) @View({ directives: [FORM_DIRECTIVES], templateUrl: 'userTemplate.html' }) class UsersAppComponent { // We instantiate the user class. user = new User(); submit(userInfo) { // HTTP request would go here console.log( this.user.name, this.user.email, this.calculateRating(this.user) ); } // Method to calculate the user's points calculateRating(userInfo) { var rating = 0; if(userInfo.javascript) { rating += 30; } if(userInfo.ruby) { rating += 20; } if(userInfo.php) { rating += 50; } return rating; } } ...
We have two-way data binding set up on our form inputs, and they are bound to the instance of our
User
model. The submit
method in our component's constructor logs the all the data to the console, including a calculation for the user's rating that it runs with the calculateRating
method. How the heck did PHP get to be worth 50 points? I guess we'll never know.Improving the Model
While this approach works sufficiently well when an application is small, the way we have set things up does have a few drawbacks:
- The
class isn't enforcing which values should be present or not. This can have implications if some operation downstream expects certain values to always be present.User
- The model doesn't have methods of its own to handle the calculation of the user's rating or the submission of the data. What if we wanted to have this same functionality in another of our app's components? We would need to duplicate the code in those other components.
- While
is trivial in this example, what if we had some important and sensitive method with business logic that is meant only for specific circumstances? We need a way to be certain that such methods can't be used in places they're not supposed to be.calculateRating
- We currently have no ability to potentially share models with a JavaScript backend.
Let's see how we can tackle some of these issues. We'll start by moving the model to its own file. We'll eventually want to end up with classes that look more like this:
// models/user.ts export class User { name: string; email: string; rating: number; constructor(userInfo:any) { this.name = userInfo.name; this.email = userInfo.email; this.rating = this.calculateRating(userInfo); } private calculateRating(userInfo) { var rating = 0; if(userInfo.javascript) { rating += 30; } if(userInfo.ruby) { rating += 20; } if(userInfo.php) { rating += 50; } return rating; } save() { // HTTP request would go here console.log(this.name, this.email, this.rating); } }
We're now using a constructor in our
User
method, which will set the values for name
, email
, and rating
when the class is instantiated. We've got our calculateRating
method in our class now, along with a save
method to take care of making an HTTP request to save the data.In our template, let's switch from using
ng-model
for two-way data binding to ng-control
to get the form values when the form is submitted.<!-- userTemplate.html --> <form role="form" #form="form" (ng-submit)="submit(form.value)"> <input type="text" placeholder="Enter your name" ng-control="name"> <input type="text" placeholder="Enter your email" ng-control="email"> <input type="checkbox" ng-control="javascript">JavaScript <input type="checkbox" ng-control="ruby">Ruby <input type="checkbox" ng-control="php">PHP <button>Submit</button> </form>
We can now modify the
UsersAppComponent
to instantiate and save a new instance of the model when the form is submitted.// app.ts ... import {User} from 'models/user'; ... class UsersAppComponent { submit(userInfo) { // Instantiate and save when the form is submitted this.user = new User(userInfo); this.user.save(); } } ...
The model and the methods that it relies on are now in the same spot and can be used in other parts of the application as well. In the model, we specified that we want
calculateRating
to be private
, but as it stands, we would still be able to gain access to it from outside the User
model. This is part of TypeScript's design, but we can put a workaround in if we really wanted to protect calculateRating
. Although it might not work as an architectural approach in every situation, we could define calculateRating
as a function within the class constructor, which would prevent it from being accessed elsewhere. The downside here is that the function is not available to other methods within the class.// models/user.ts ... constructor(userInfo:any) { this.name = userInfo.name; this.email = userInfo.email; this.rating = calculateRating(userInfo); // Function is only available inside the constructor // when the logic really needs to be protected. function calculateRating(userInfo) {...} } ...
Moving to Factories
We've seen how we can instantiate a new model and access methods on it, but what if we wanted to take the factory approach? In this approach, instead of instantiating the model directly in our components, we would call a method like
create
on the factory. As applications grow and classes become increasingly complex, this can have certain advantages, especially for maintainability. For example, if we wanted to change the name of a class that gets instantiated, the factory approach allows us to make a change solely to the factory itself and not all the instantiation points around the app.This is also where Angular 2's Dependency Injection comes in, as we'll need to inject the factory to make use of it.
// models/userFactory.ts import {User} from './user'; export class UserFactory { // Uses the User model to create a new User create(userInfo:any) { return new User(userInfo); } }
We'll need to inject the
UserFactory
into our component's constructor. Angular 2 comes with an @Inject
decorator, which is used to attach metadata to the component. We also need to include UserFactory
in an array when the app is bootstrapped so that it is included as a binding.// app.ts ... import {UserFactory} from 'models/userFactory'; ... class UsersAppComponent { userName: string; userRating: number; rating: number; // We inject the UserFactory constructor(@Inject(UserFactory) UserFactory) { this.UserFactory = UserFactory; } ... } // We bind the UserFactory when the component is bootstrapped bootstrap(UsersAppComponent, [UserFactory]);
With
this.UserFactory
pointing to the UserFactory
, we can now make use of its methods. Let's adjust the submit
method to use the factory.// app.ts ... submit(userInfo) { this.user = this.UserFactory.create(userInfo); this.user.save(); } ...
Now when the user submits the form,
create
and save
are called with the form data.We can also use the model approach to get the data for the user that just submitted the form. For example, we could add a
get
method to the User
class that returns (or in this case logs to the console) the user's data.// models/user.ts ... get() { console.log(this.name, this.email, this.rating); } ...
Then within the component we can create a
getUser
method to retrieve it.// app.ts ... getUser() { this.user.get(); } ...
More on Dependency Injection
We used the
@Inject
decorator to bring in UserFactory
above; however, TypeScript also allows us to use type annotations, which means the constructor can be written as:// app.ts ... constructor(UserFactory: UserFactory) {...} ...
The array syntax we used to bind
UserFactory
to our component is the shorthand way of doing it. We can also explicitly state what we want to bind to, which gives us the option of binding to other classes. For example, we could bind User
to UserFactory
directly.// app.ts ... // The UserFactory can be bound to another name, in this case, User bootstrap(UsersAppComponent, [bind(User).toClass(UserFactory)]); ...
With this,
UserFactory
becomes User
when used in our component.Aside: Using Angular with Auth0
Auth0 issues JSON Web Tokens on every login for your users. This means that you can have a solid identity infrastructure, including Single Sign On, User Management, support for Social (Facebook, Github, Twitter, etc.), Enterprise (Active Directory, LDAP, SAML, etc.) and your own database of users with just a few lines of code. We implemented a tight integration with Angular 1. Angular 2 integration is coming as soon as it's on Beta! You can read the documentation here or you can checkout the SDK on Github.
Wrapping Up
It's important that thought be given to how applications are architected so that they are easier to maintain and can be collaborated on by more developers as team sizes grow. It's also important to protect business logic and keep sensitive methods from being accessed outside the class to which they belong. These effects can be achieved by moving logic into models.
Angular 2 gives us a new Dependency Injection system that allows us to inject classes into our components easily. We had a brief look at how the new DI system works, but for a more thorough overview, we recommend you check out Pascal Precht's post on Dependency Injection in Angular 2.
About the author
Ryan Chenkie
Developer
Ryan is a Google Developer Expert, the host of the Entrepreneurial Coder Podcast, the author of Securing Angular Applications, and an all-around fanatic about application security.View profile