Designing Angular architecture - Container-Presentation pattern

Post Editor

Designing architecture could be tricky, especially in the agile world, where requirement changes are frequent. So your design has to support that and provides extendibility without the need for serious modification. In such cases, you will find the Container-Presentation pattern instrumental.

22 min read
4 comments
post

Designing Angular architecture - Container-Presentation pattern

Designing architecture could be tricky, especially in the agile world, where requirement changes are frequent. So your design has to support that and provides extendibility without the need for serious modification. In such cases, you will find the Container-Presentation pattern instrumental.

post
post
22 min read
4 comments
4 comments

Did it ever happen that you developed a new feature, then later new requirements came in, and you needed to change the components a bit, tweak some logic, extend some data structures? It's a widespread situation while working in agile environments, right? That's why it is essential to realize from the beginning that the changes can occur, and while working on an initial feature, you should design your solution so it can be effortlessly extended in the future.

It's necessary to see the difference between designing a solution with openness for extending and actual coding elements that may be helpful in the future but aren’t needed right now. I am encouraging you to don't write unnecessary code - you can’t predict future changes. The point is to plan and implement an architecture that will simplify extending, no matter how the requirements evolve.

That's why you need to focus on a proper design solution before starting to code it. You have to allow yourself to see the bigger picture, don't focus for a while on specific details. Instead, try to see how the components should correlate, what will be the information flow, what classes will be involved in the process, and what their responsibilities will be.

Design Patterns
Link to this section

When planning architecture for a new feature, it is beneficial to know the most common cases - called the Design Patterns. They are basically a set of common architecture guides that should apply in most cases. To save your time - you should definitely get to know them. I'm not implying that you should learn them by heart right away - fluency in using them will come naturally while working with them, so do not worry about that. Instead, try to think about your features and tasks, having in mind various patterns that can help you.

Crucial Design Points
Link to this section

I want to highlight a few crucial points from my point of view. When planning architecture for a new feature, I'm always trying to make decisions on those points, preferably before starting to code. These key points are:

  • What components, services, and other classes will be used (do I need to create new ones? are there any classes that I can reuse?)
  • What are the responsibilities of those classes? Are they limited enough?
  • What is the data flow between those classes, especially the components?
  • What are the dependencies in the components?
  • Is my plan open for extension in the future? (this is very similar to "O" in the SOLID principle)

Why Container-Presentation?
Link to this section

The pattern that I want to introduce to you is called the Container-Presentation, and it clearly answers all those points above. Why have I chosen that particular pattern? I think this is an instrumental design that can be applied on an almost everyday basis - I believe many of you already intuitively use it in your feature design which is excellent. I want to describe it clearly and show step by step how it can be applied and extended. Although the use case for that pattern is very common, developers tend to omit important parts of that pattern which I will highlight and try to convince you to include in your implementation.

Before digging into the pattern architecture, I would like to highlight the key reasons why this pattern could be useful for you:

  • it provides flexible and open for extension split of the components
  • separation of the components makes it easy to apply efficient change strategy to each type of the component
  • component’s dependencies are limited and well organized, which makes them easy to manage, preferably some of the components don’t have any dependencies at all!
  • most of the components are highly generic, and the pattern itself compels you to create them so that they can be easily reused in future work
  • logic and UI are separated, which means it’s pretty safe to change the UI without causing any issues with the implemented logic for the feature

So let's get started!

Container-Presentation pattern
Link to this section

This particular pattern is well suited for the Agile development approach, and I will prove it to you.

The rest of the article will be split into stages, to show you how the pattern evolves while remaining the same structure and how easy it is to extend the approach when the environment changes - new requirements come in.

Example - CRUD view
Link to this section

Abstract solutions are always hard to follow, so that I will use a simple example for the sake of that article. Imagine that a new feature to develop is a basic CRUD view for users. Consequently, the result will be a fully working view that presents a list of the users, with the ability to edit and add new ones.

example app

The requirements for the app are listed below and will be answered by the following sections of the article.

  1. View for presenting a list of the users
  2. Ability to edit an existing user by selecting it from the list and edit using a view next to the list
  3. Ability to create a new user and add it to the list of the users

Foundations of Container-Presentation pattern
Link to this section

What is the first step you need to consider when thinking about the Container-Presentation pattern? First of all, you need to split your UI into components. How to do that? Start from the most obvious one, the main one most likely, and then apply the divide & conquer paradigm to split the task into smaller ones which should go along with creating new components dedicated for more precise jobs.

That’s the theory. However, it’s often the case that it’s relatively hard to plan everything before implementing any of these components. So instead, I would suggest starting from implementing roughly the main component or a few main ones - that depends on your preferences and complexity of the feature and then use the divide & conquer paradigm iteratively, which will ultimately lead to splitting the task into appropriate components.

When you implement your first component or set of components (which probably won’t follow the pattern yet), you can start thinking about how to refactor them to follow the Container-Presentation way.

When you have base components, it's simply a matter of considering which of them will be a "Container" component and a "Presentation" component. How to distinguish that? You should think about these two types of components as the components that do actions and distributes the data across, and the components that simply show some data and optionally handle user input - like mouse click for instance. The common mistake here is believing that if a component receives an event from a user, it needs to react with exact action, call a service, process some data, etc. In our pattern, it is the other way around. In most cases, the component should only inform its Container about the user event, no need for doing much else. The Container is responsible for handling the event appropriately.

To sum up that very shortly:

  • Container: takes care of the data, distributes that, handles service calls and most of the logic
  • Presentation: present data, sometimes receives events from the users and passes them to Container component

The image below presents the basic flow using the Container-Presentation pattern:

Container-Presentation diagram

The pattern doesn't require any complex logic, classes, or any other complicated layers of abstraction. In the simplest case, it only uses Input/Output component communication. In the basic flow, the only Service required is the one that populates the data for the Container component.

Stage 1: a bunch of variables
Link to this section

Let's get back to our example: CRUD view for users. Imagine that the first requirement for that feature is to create a view that presents a list of users in the form of a table fetched from the API. Flats below present the idea:

example app - first stage

So how to plan the architecture, keeping in mind we want to apply the Container-Presentation pattern?

I will start by implementing the most obvious solution - so a single component will do the task.

The component class could look like this:

<>Copy
@Component({ selector: 'app-users', templateUrl: './users.component.html', styleUrls: ['./users.component.css'] }) export class UsersComponent implements OnInit { users; constructor(private data: DataService) {} ngOnInit() { this.users = this.data.getUsers(); } }
users component

The template will simply present the users:

<>Copy
<article *ngFor="let user of user"> <p>{{user.name}} {{user.lastName}}</p> <p> <span *ngFor="let tag of user.tags">#{{tag}}</span> </p> <button>Edit</button> <button>Delete</button> </article>
users component template

We could simply leave it as a single component that will do everything. What are the cons?

The component class will grow by time rapidly, the business logic will be mixed up with the presentation logic. It would probably be tough to use an efficient Change Detection strategy because of the many trigger points from different places. Moreover, the view will be hard to extend because the logic will be very coupled in this single component - an example of a poorly planned part.

Let's try to break it down. We need a component to present user info - that's for sure. Does this component need to fetch the data? Well, it looks like a different responsibility, right? So we should create another class - a wrapper Component for that purpose. We now have two components - one for presenting user info, second for fetching the data. Clearly, we ended up with one Presentation Component and one Container Component. That's great!

Here is a simple diagram to make sure we are on the same page:

Container-Presentation diagram in context of example app

No complex communication between the components is needed for now - it’s just a uni-directional flow. Container Component fetches data via Service, resolves it, and passes to Presentation Component via Input property. Finally, the Presentation Component displays the data, and requirements are fulfilled!

Alright, enough of the theory, now the code:

I will start from the Presentation component because the implementation shouldn't rely on the Container design, so I always start by implementing these components with a fresh head.

We need a component which will display simple data in form of a tile, the data should be delivered via Inputs and that's all that we should care on this stage of implementation.

<>Copy
@Component({ selector: 'app-user-tile', templateUrl: './user-tile.component.html', styleUrls: ['./user-tile.component.css'], changeDetection: ChangeDetectionStrategy.OnPush }) export class UserTileComponent { @Input() name: string; @Input() lastName: string; @Input() tags: string[]; }
user tile component

It looks straightforward, and that's our aim! Why overload components with logic, complex processing, or other stuff - we should keep components as simple as possible, and such Presentation component is very clear to read and maintain. It’s worth to mention this component doesn't rely on any advanced Types. It uses primitives in Inputs, doesn't implement any interface, and finally, it can use an effective Change Detection strategy, OnPush.

Simple, efficient, maintainable component!

<>Copy
<article> <p>{{name}} {{lastName}}</p> <p> <span *ngFor="let tag of tags">#{{tag}} </span> </p> <button>Edit</button> <button>Delete</button> </article>
user tile component template

There are no surprises in the template - as in design flats, we are displaying the user’s full name, along with the tags assigned and two buttons that will be handy later.

Now we can jump into the Container component. It has to fetch the data using Service, and it has to show the list of the users. Well, we already have a component for presenting a user, so it should be pretty straightforward.

<>Copy
@Component({ selector: 'app-users', templateUrl: './users.component.html', styleUrls: ['./users.component.css'] }) export class UsersComponent implements OnInit { users; constructor(private data: DataService) {} ngOnInit() { this.users = this.data.getUsers(); } }
users component

I believe no explanation is needed - very clear and simple component with data stored in users property.

<>Copy
<app-user-tile *ngFor="let user of users" [name]="user.name" [lastName]="user.lastName" [tags]="user.tags"> </app-user-tile>
users component template

The template is simple as well! It just renders the list of user-tile components and correctly passes the data.

Guess what? That is all the code needed to fulfil our requirements, and this is the only code required for the Container-Presentation pattern.

In the following sections, I will prove that the pattern is useful when requirements change and the whole solution gets more complex, but as long as we follow the pattern, we will be safe.

So, to sum up, that first step, we did:

  • We created a simple Presentation component that has as simple Inputs as possible and OnPush Change Detection Strategy - which results in a very efficient component with a clear and straightforward way to use, which in addition is not coupled with any existing dependencies in the application.
  • We created a Container component responsible for getting the data and using presentation components to render that, by passing correct properties into it.

Stage 2: Model
Link to this section

The new set of requirements just came!

First, we need to allow users to edit a user, resulting in presenting editable details of that user and the ability to save revised data. Also, the currently selected user should be highlighted on the list.

Let's take a look at the flats:

example app - stage 2

Based on what we know from the previous step, it should be clear how to apply changes to the architecture design we already have.

  • Container (users-component) - has to keep track of user which is currently being edited
  • Presentation (user-tile-component) - has to be highlighted when it's the one being edited

What should we do about the editing view on the right? First, there should be another Presentation component. Second, it should receive the data needed to be present, allow data to be in the editable form, and finally inform the Container about possible changes.

Why couldn't it just save the edited user itself? Well, let's see what would happen. The presentation component would need to work closely with the Data Service. It would need to save a user, wait for the response and then inform the parent about that to refresh the table view. It is unnecessarily complicated! We should keep the editing component as the Presentation component. Therefore it will not be coupled with any Service, it will not have to do any complex logic and processing of the data, and finally, it will not have to notify others that they need to refresh themselves. The container is the only class that requires a dependency which is our Service, and it's the best place to identify when and how to refresh the view.

I will once again start with implementing Presentation components, and first of all, I will begin by making changes to the user-tile component. Next, I need to pass additional information about whether the tile is selected and needs to be highlighted.

<>Copy
export class UserTileComponent { @Input() name: string; @Input() lastName: string; @Input() tags: string[]; @Input() active: boolean; // <--- new property }
user tile component

Then the template could react on that like this:

<>Copy
<article [class.highlighted]="active"> <p>{{name}} {{lastName}}</p> ... </article>
user tile component template

That would work nicely, but did you realize that I was forced to add additional Input property when requirements changed? That's not the best, because later I can end up with dozens of those, which are not readable and easy to maintain. That is when the Model from the section title comes in handy. It’s often the case we are dealing with properties that belong to a specific context, and such context is usually called a Model. The Model definition usually represents a business model in the application. That approach is commonly used in the Domain Driven Design, where the Domain Entity is a foundation block for that concept, and it’s a base for a Model. In various situations, it comes with different names/approaches, but the idea stays the same.

How can it apply in our case? Well check that class implementation:

<>Copy
export class UserTileComponent { @Input() vm: { name: string, lastName: string, tags: string[], } @Input() active: boolean; }
user tile component

Why is the property called vm? VM is the conventional abbreviation for View Model, a convention to use a single Input property, an object that consists of multiple properties. Thus, the name of the property is in some cases called "vm".

What are the benefits? It's a single Input property, so that it will trigger Change Detection only once. It will tell you if you pass input in the wrong type, and you won't forget to pass any needed property. Of course, you can extract that definition into your own Type to make it even more readable. It will be tricky to introduce active property into the vm object later, so I left him outside, but you get the idea.

It looks like a strategy to receive data is in place. We need to add code to inform the Container about licking on the "Edit" button. There is no need for that Presentation component to do anything with the information that the user clicked on that button but sending an event outside. The presentation component could be used in different scenarios in different use cases, so the exact logic of what happens next can differ. That’s why we only inform about such events and leave implementation for the Container.

<>Copy
export class UserTileComponent { ... @Output() selected = new EventEmitter(); select() { this.selected.emit(); } }
user tile component

Presentation components, as I said before, use Outputs properties to inform Containers about the events. Then, of course, the button from the template has to call the select function on click. That’s it.

The second part of that task was to introduce an ability to edit a user. For that I will create a template with simple form:

<>Copy
<form> <input type="text" [(ngModel)]="vm.name"> <input type="text" [(ngModel)]="vm.lastName"> <button (click)="onSave()">Save</button> </form>
user form component template

This component will be a Presentation component as well, so as you may already realize - Inputs and Outputs will be handy. No logic hidden inside, simply receive data and pass the event through.

<>Copy
@Component({ selector: 'app-user-form', templateUrl: './user-form.component.html', styleUrls: ['./user-form.component.css'], changeDetection: ChangeDetectionStrategy.OnPush }) export class UserFormComponent { @Input() vm; @Output() save = new EventEmitter(); onSave() { this.save.emit(this.vm); } }
user form component

This is the point when you should spot one issue with this code. Do you see that?

It's about passing the data - we will use an object representing the Model to gather all of the input data together, so we will then change the values implicitly. The vm object is passed by reference. This will result in mutating the original data. So when we edit, for instance, the name, it will change in both input and the table (which has the original data)!

That's obviously wrong. We shouldn't mutate original data. Instead, we should make a copy of it, edit it, save it, and refresh the data in the table.

Who should be responsible for making a copy? As we said in the Container-Presentation pattern, only one component is responsible for distributing correct data, making the Container accountable.

How should the copy be made? There are a few different approaches to cloning objects in JavaScript. The first difference is in how advanced the clone needs to be. For instance, in our case, vm is a plain object with a few properties that are not objects themselves. For such a case, we could use shallow cloning. However, deep cloning will be preferred when the data you need to clone is nested and requires you to clone it on each level.

For shallow cloning, I will propose to use a spread operator. For deep cloning, you may find it helpful to use external libraries like lodash or clone or use JSON object built-in web API.

In our example, I will use a simple spread operator to clone the data.

Let's take a look at the Container implementation. The template looks pretty standard:

<>Copy
<app-user-tile *ngFor="let user of users" [vm]="user" [active]="selectedUser?.id === user.id" (selected)="selectUser(user)"> </app-user-tile> <app-user-form *ngIf="selectedUser" [vm]="selectedUser" (save)="save($event)"> </app-user-form>
users component template

I added our new Presentation component for editing selected users.

<>Copy
@Component({ selector: 'app-users', templateUrl: './users.component.html', styleUrls: ['./users.component.css'] }) export class UsersComponent implements OnInit { users; selectedUser; ... selectUser(user) { this.selectedUser = {...user}; } save(user) { this.data.edit(user); this.users = this.data.getUsers(); } }
users component

Look closely at how the method selectUser changed. It now does a little more than just assigning selectedUser value. It uses a spread operator to clone the data first, so wherever we use selectedUser, it won't be referenced to the original data.

The newly added method save is pretty straightforward. It gets the edited data, uses the service to edit the user, and updates data at the end.

That's all - we again fulfilled the requirements, using very little code, and what's more important, we again used the Container-Presentation pattern. It didn't require many changes, as you saw. It was simply a matter of adding new elements, not really changing the ones implemented before. That's, in my opinion, the best sign that our architecture is well planned and implemented. We are open for extension and close to modifications!

To sum up, the key things that come from that section:

  • Model - a definition representing a domain/business model, commonly used as a single Input in Presentation Components. Making it more readable, type-safe, and ensuring it will have all of the required properties.
  • vm - abbreviation for View Model, which can be used to represent a Model
  • cloning - the need for cloning the data is a common situation. There are different techniques - for shallow cloning, use spread operator.

Stage 3: Model Class
Link to this section

New requirements came in! The creation of users is required as the next step.

Let's look at what we have on the plate currently. There is a list view and the form view that handles the edit. The creation of users will use pretty much the same component as for edit. The only difference is, we need to be able to distinguish between editing of existing users and the creation of new ones. How to do that? Many options here, but I will use the id approach. So imagine each user has its own unique id, which differentiates it from a user who is just being created - it won't have the id unless it is submitted.

This takes us to three conclusions. One is that the user definition is getting bigger. Secondly, some properties will exist or not depending on the context, and at last, we need to be able to create a blank user and fill it with data during the creation process.

This leaves us with no choice but to learn how to introduce this to our architecture easily. Actually, it's straightforward. First, we have to add a Class. That Class will keep all properties together, provide information about the optional and required properties, and finally, make it very easy to create a blank object.

That's how the Class can be implemented based on our needs and previous assumptions.

<>Copy
export class User { constructor( public name: string = '', public lastName: string = '', public tags: string[] = [], public id?: string ) {} }
user

That class has all three properties that we've used before, plus an optional id. It can be created via a constructor like that:

<>Copy
const blank = new User();
blank user creation

Which will result in a blank user, ready for the creation process. That's all we need.

Now we can move on with the components adjustments:

The Container will need a button for starting the creation process, and it will need a method that creates a blank user for the Presentation form component.

<>Copy
<button (click)="create()">Create</button> <app-user-tile *ngFor="let user of users" [vm]="user" [ [active]="selectedUser?.id === user.id" (selected)="selectUser(user)"> </app-user-tile> <app-user-form *ngIf="selectedUser" [vm]="selectedUser" (save)="save($event)"> </app-user-form>
users component template

The button is in place, now the logic:

<>Copy
@Component({ selector: 'app-users', templateUrl: './users.component.html', styleUrls: ['./users.component.css'] }) export class UsersComponent implements OnInit { users: User[]; selectedUser: User; ... selectUser(user: User) { this.selectedUser = {...user}; } create() { this.selectedUser = new User(); } save(user: User) { if(user.id){ this.data.edit(user); }else{ this.data.create(user); } this.users = this.data.getUsers(); this.selectedUser = null; } }
users component

As you see, I also added a User type to the component class to make it more safe and bulletproof.

The Container is ready. What needs to be changed inside the Presentation form component?

Well, nothing! It works the same. It receives a User (this time blank), it emits an event when the user wants to submit it, and that's it. That's why I want you to learn this pattern because every time the requirement changes - it needs only a slight extension, and everything else works perfectly fine.

Stage 4: VM class with extras
Link to this section

At this stage, I won't bring any new requirements. Instead, I will focus on few improvements in our code and app. Implementation of all these changes will follow our pattern design.

These will be:

  • extracting cloning logic
  • improving access for name and last name
  • improving how the empty users are being created
  • implementing a validation

Cloning - Do you remember we needed to clone the user object for editing purposes? It was nicely done, but I think the responsibility of cloning could live somewhere else. It is currently implemented in the Container, but is it really its responsibility to know how to clone the object? Not really, it must clone the object but not necessarily provide a cloning implementation algorithm.

How it looks now?

<>Copy
selectUser(user: User) { this.selectedUser = {...user}; }
users component

I will move the cloning implementation to the class itself and leave the Container free of that problem.

<>Copy
export class User { clone(): User { return new User(this.name, this.lastName, this.tags, this.id); } constructor( public name: string = '', public lastName: string = '', public tags: string[] = [], public id?: string ) {} }
user

Now the Container class code is more superficial and without unnecessary dependency. It also is a bit more readable.

<>Copy
selectUser(user: User) { this.selectedUser = user.clone(); }
users component

Now, do you remember how are we displaying name and last name in the list? Let's take a look:

<>Copy
<article> <p>{{vm.name}} {{vm.lastName}}</p> ... </article>
user tile component template

It may often be the case that these two properties will need to be rendered together. To remove possible code duplication, we can create a single getter function that will always resolve these properties in a correct format. How to do it? We could create a Pipe for that, but we already have vm Class we could use! Let's keep in the same place then:

<>Copy
export class User { clone(): User { return new User(this.name, this.lastName, this.tags, this.id); } get fullName(): string{ return `${this.name} ${this.lastName}`; } constructor( public name: string = '', public lastName: string = '', public tags: string[] = [], public id?: string ) {} }
user

Now the template of the Presentation tile component can be simplified:

<>Copy
<article> <p>{{vm.fullName}}</p> ... </article>
user tile component template

Isn't that great? We are only adding tiny functions into the vm Class, and the whole solution works great without the need for much refactor.

I think you now understand how the vm Class should be used, but just to clarify and show its full potential - two more examples.

Remember how we create a blank user object inside Container for creation purposes? It's the same situation as for cloning. There is no reason for Container to take care of creating a blank user - let's extract that logic into the vm class!

<>Copy
export class User { static createBlank(): User { return new User(); } clone(): User { return new User(this.name, this.lastName, this.tags, this.id); } get fullName(): string{ return `${this.name} ${this.lastName}`; } constructor( public name: string = '', public lastName: string = '', public tags: string[] = [], public id?: string ) {} }
user

The Container will simply call the static function in order to create a blank object - no need to care about how it's done.

<>Copy
create() { this.selectedUser = User.createBlank(); }
users component

The last improvement is about validation - in our Container component, we aren’t checking whether the submitted user is a valid one. Assume the valid user should have a not empty name and last name. Where should such logic be implemented? Of course, in our vm Class!

<>Copy
export class User { static createBlank(): User { return new User(); } clone(): User { return new User(this.name, this.lastName, this.tags, this.id); } get fullName(): string{ return `${this.name} ${this.lastName}`; } get valid(): boolean { return this.name.length > 0 && this.lastName.length > 0; } constructor( public name: string = '', public lastName: string = '', public tags: string[] = [], public id?: string ) {} }
user

Then it can be simply used in the Container to check whether the object can be submitted for creation or edition:

<>Copy
save(user: User) { if (!user.valid) { return; } if (user.id) { this.data.edit(user); } else { this.data.create(user); } this.users = this.data.getUsers(); this.selectedUser = null; }
users component

Believe me, such techniques will help you very often, and by designing the application properly, you won't counter many issues with new requirements or refactor. It's all about extending the core pattern every time you need it.

Stage n: complex scenario with Service
Link to this section

What if a single Container is not enough? What if the feature grows and we need to introduce more complex logic, split it into multiple smaller sub-features, and as a result, we end up with a bunch of components that are nested within each other for a few levels deep? For sure, It's not a problem with our pattern! It just needs some adjustments, as always. The core - so the relation between the Containers and Presentation components stays identical - that's the key. Don't be scared of creating many Containers - it is the right thing to do in complex scenarios. The main question is whether passing the data through Inputs and Outputs still makes sense.

In some cases, for some component groups, it will but, maybe not for the entire solution. For instance, there could be cases when multiple Models have to be combined to fulfill some requirement. Such processing could happen in Services, and they can be used for Containers to provide specific fragments of the data needed for that part of the feature.

However, if you use the pattern, it will be relatively easy to add that layer. Moreover, there are a few approaches to tackle such cases. For example, in some situations, all you need is a Model class done with the OOP approach or service that manipulates POJOs, which will use a functional approach and make the data containers immutable. Such approaches are an interesting topic, and I will explain them in the following articles.

Possibilities are enormous, it can grow and grow, but the core principles are the same as for the rule of extending implementation when needed.

Summary
Link to this section

I hope you enjoyed that little story about the Container-Presentation pattern and learned new stuff. I’m sure you were familiar with most of those techniques, but bringing them together to make a solid architecture is the key.

What were the main ingredients in this guide:

  • Container Component- responsible for retrieving data and distributing them to Presentation Components
  • Presentation Component - responsible for rendering data and optionally informing Container about events (for instance, user actions)

What should be used for communication between these two types of components?

  • Input / Output for most cases
  • Service for rather complex scenarios

The other concepts I used:

  • vm - View Model convention for wrapping Input properties into a single object. It should be typed to increase safety.
  • Change Detection OnPush - the most efficient strategy for most cases, usually can be used without issues in Presentation Components
  • vm Class - if the model grows, or it needs to keep some logic to either remove code duplication or extract the logic from other places, it is recommended to create a Class with appropriate properties, getters, functions, etc.
  • cloning techniques - it's usually important to clone the objects that we work with, not work on the same data instances. You need to think if your solution requires shallow or deep cloning and use the appropriate algorithm. Spread operator can be useful for shallow cloning.

Lastly, I want to remind you of the key idea that goes through this article and is essential. Introducing such patterns in your architecture will make the solution more solid, concise which will help you with:

  • creating a code without duplication
  • providing clear responsibility for each class
  • being ready for future requirement changes - which means it will be easy to extend your architecture without the need to modify existing code

If you are looking for the final code, here is a stackblitz.

Inspiration for that article came to me during this year's ng-conf. The pattern caught my attention at a workshop run by John Papa and Dan Wahlin - thank you, guys!






















Comments (4)

authorchristian-nagel
8 September 2021

Really great article. Very useful tips and advices.

Bye...I spent now some focus time to refactor some files and architecture ;-)

Cheers

authorMaciejWWojcik
9 September 2021

Thank you. Glad, you found that useful! 🤓 enjoy ya refactor 🙌

authorArthur-Fedotiev
10 September 2021

Thanks for valuable content!! Those are the things that I lately trying to implement when working on the features (smart-dumb, and VM). Though I've got a problem to understand the thing with services. In example, if I'd had some feature structure like the following one: Component1(smart) Child1(dumb)---Child2(dumb) GrnadChild1(dumb) GrandChild2(dumb) I would go ahead and create one service provided on the level of Component1. Dumb components receive VM via inputs, and pass messages about user interaction to that service which communicates results and new data to the smart ComponentA. I've got the following concerns:

  1. Is it correct to inject that service into dumb components and use it like this? Are components still dumb if they use such a service?
  2. Is it right to create a service for a feature business logic at all? Or is it sime kind of coupling between smart component and that service?
  3. What techniques could be considered for achieving proper smart-dumb architecture, true decoupling, and lean smart component that doesn't have lots line of logic in it? Regarding this one, I believe that you gave a hint mentioning several smart components, but I am still completely lost how to properly implement this architectural approach when "having smart components inside dumbs" kind of situations
authorMaciejWWojcik
10 September 2021

Thank you! 🥳 So answering your concerns:

  1. In my opinion it depends. I often try to remove such dependencies from the child (dumb) components, which makes them really dumb and easy to debug if anything is wrong, but I also see it's not always possible, or should I say, it's not always the best. If the child is nested deeply, or there is much logical sense of passing events through some components I think injecting such services helps and reduces the complexity of the overall solution.
  2. I would say that it's definitely right! In my opinion, extracting such logic into services makes it easier to unit tests, and while components are responsible for the presentation layer, the service keeps business logic. Smart components in that case stay "smart" but as smart presentation elements, they just receive more "processed" data.
  3. For me it's mostly a matter of thinking about the component responsibility - I like to create components that are limited and present/handle a single "thing". About architecture where smart components are inside dumb ones - it's often a case when models interlace, for instance, imagine our "users view" example and the "tags" inside a user - if the tags are a completely separate feature, a complex one, then probably editing/adding those "tags" will need it's own smart-component and dumb ones which then will be used inside dumb - user creation component.

Hope it makes sense to you 🤓

authorheynitish717
15 September 2021

Great and well defined approach Maciej. Loved the way you handled Singleton pattern of SOLID principle by putting all 'User' class specific functions inside the class itself.

authorMaciejWWojcik
15 September 2021

Thank you! 😊

Share

About the author

author_image

Hey, I'm a frontend developer, passionate about good design for both the code and UX/UI side of things. I am mainly involved in Angular, rxjs, typescript subjects.

author_image

About the author

Maciej Wojcik

Hey, I'm a frontend developer, passionate about good design for both the code and UX/UI side of things. I am mainly involved in Angular, rxjs, typescript subjects.

About the author

author_image

Hey, I'm a frontend developer, passionate about good design for both the code and UX/UI side of things. I am mainly involved in Angular, rxjs, typescript subjects.

Looking for a JS job?
Job logo
PDQ team| Senior JavaScript developer (Angular/Node)

SD Solutions

Ukraine
Remote
$60k - $80k
Job logo
Senior Full-Stack Developer (Node+Angular)

A-Listware

Ukraine
Remote
$48k - $78k
Job logo
Senior Full stack (Angular+Node)

Monolith

Ukraine
Remote
$60k - $84k
Job logo
AngularJS Developer/.net Core - Remote Contract

InfoMagnus

United States
Remote
$115k - $134k
More jobs
NxAngularCli
NxAngularCli
NxAngularCli

Featured articles

Angularpost
13 September 20218 min read
Tracking user interaction area

Explore one of the most complex pieces of Taiga UI — ActiveZone directive that keeps an eye on what region user is working with. It touches on low-level native DOM events API, advanced RxJS and Dependency Injection, ShadowDOM and more!

Angularpost
13 September 20218 min read
Tracking user interaction area

Explore one of the most complex pieces of Taiga UI — ActiveZone directive that keeps an eye on what region user is working with. It touches on low-level native DOM events API, advanced RxJS and Dependency Injection, ShadowDOM and more!

Read more
AngularpostTracking user interaction area

13 September 2021

8 min read

Explore one of the most complex pieces of Taiga UI — ActiveZone directive that keeps an eye on what region user is working with. It touches on low-level native DOM events API, advanced RxJS and Dependency Injection, ShadowDOM and more!

Read more
micro frontendspost
6 September 202125 min read
Taking micro-frontends to the next level

The micro-frontends concept has been out there for quite a while. We’ve been using this architecture in Wix since around 2013, long before it was even given this name. In this article I’d like to share some of the things we did in order to evolve the concept of developing big scale micro-frontends.

micro frontendspost
6 September 202125 min read
Taking micro-frontends to the next level

The micro-frontends concept has been out there for quite a while. We’ve been using this architecture in Wix since around 2013, long before it was even given this name. In this article I’d like to share some of the things we did in order to evolve the concept of developing big scale micro-frontends.

Read more
micro frontendspostTaking micro-frontends to the next level

6 September 2021

25 min read

The micro-frontends concept has been out there for quite a while. We’ve been using this architecture in Wix since around 2013, long before it was even given this name. In this article I’d like to share some of the things we did in order to evolve the concept of developing big scale micro-frontends.

Read more