Better Programming

Advice for programmers.

Creating an Angular Web App for Multiple Views and Screen Sizes

How to create a web app in Angular that looks and feels great on every screen size

Yosi Golan
Better Programming
Published in
17 min readNov 23, 2017

--

Angular is probably the best platform to create a great web app. But how can we create a web app that will look and feel amazing in any resolution? In this article, we will learn how to use traditional software architecture and design patterns to harness the power of the Angular platform to create a state-of-the-art web app that can be viewed in any device at any resolution.

The techniques that we’ll use require Angular 4 and up.

The described sample app is available at https://github.com/yosigolan/angular-multi-view

Motivation

HTML and web resources are designed to be viewed in any device that has an internet browser. Over the last few years, the varieties of devices and screen sizes (and resolutions) have grown exponentially. When the first mobile phones with internet browsers appeared, the response of site creators to the different screen sizes was the adaptive site.

Adaptive site means that the site view is adjusted according to screen width, but at specific thresholds. For example, a desktop version would be adjusted at a resolution higher than 768 px and mobile versions at lower resolutions. If we browse to a page using our mobile device, the mobile version will be presented, and if we browse to the page using a desktop pc, the desktop version will be presented.

Wikipedia old adaptive site — mobile view vs. desktop view

But then, with the birth of smartphones, more devices appeared with more screen sizes (phones, tablets, and so on). HTML’s answer to support different screen sizes was responsiveness. Web site creators used CSS techniques to create a responsive site that adjusts itself to the screen width at any resolution point. A responsive site means that the HTML is written usually using a grid. Then, using CSS, the grid renders the HTML according to screen size, so that whenever two elements can’t reside one next to the other (horizontally), they appear one after the other (vertically), for example:

Responsive site — mobile view vs. desktop view

While a responsive site saves a lot of work (you write the HTML only once), it’s not always sufficient. With the emergence of strong web platforms such as Angular, websites today function more and more like web apps and can do much more than only providing data to the user. Users anticipate viewing a web page that looks and acts like a mobile app. The mobile app look is usually completely different from that of a desktop web site. Therefore we must have a completely different view for it and can’t use the responsive approach.

It sounds as if, for a modern web app, we need the old adaptive approach. But that is still not enough. The reason is that still there are more screen sizes than we can support. We can’t create an adaptive view for each screen size.

The answer is hybridization between the adaptive approach and the responsive approach. What we really want is a web app that has a few views for a few screen size levels, for example, a view for all resolutions higher than 768 px and a view for all resolutions lower. Up until here, it’s the adaptive approach. Inside each view we want it to be responsive so that all resolutions above 768 px will show the same view, but be responsive to the current resolution. That way we cover the entire resolution spectrum while minimizing the view versions we create.

SeeVoov high resolution view responsiveness
SeeVoov low resolution view responsiveness

Options using Angular

So let’s figure out how we do it in Angular. Angular is component-based, so at the lowest level, we would like to change the view of a component. Let’s review the possible options we have for a view change in a component according to screen size.

Option 1: The component view is responsive.

In this case, we don’t need any of the techniques in this article. We can just use the well-known CSS way to make a page responsive.

Option 2: The component has a different view according to screen size, but the component logic is different between the views.

This is a problematic case. From a business point of view, it means that the users receive different experiences between the views, including different logic. That can be confusing. The only thing that is common here is the route of the page. I suggest avoiding such cases, but sometimes it a must.

Option 3: The component has a different view according to the screen size, while the component logic is the same in all views.

This is the big cherry. In this case, we need different HTML and CSS for every screen size. However, the logic behind it is identical for all sizes and we want to reuse the same code for all views. For an example, see the SeeVoov site:

SeeVoov — mobile vs. desktop

Now that we have listed all the options we have, let’s figure out how to implement the view changes for each one of them.

In this article, for demonstration purposes, we will create a sample app that present products on its front page. Clicking on a product will route to the product details page. In addition, we will have an About page and user profile area. The sample app will cover all the options discussed above. The code can be found here: https://github.com/yosigolan/angular-multi-view

Infrastructure

The first thing we need to take care of is infrastructure. Whenever we need to select a view, we’ll require the current view kind. To achieve that, we’ll create an ApplicationStateService that will hold and set it.

application-state.service.ts:
import { Injectable } from '@angular/core';

@Injectable()
export class ApplicationStateService {

private isMobileResolution: boolean;

constructor() {
if (window.innerWidth < 768) {
this.isMobileResolution = true;
} else {
this.isMobileResolution = false;
}
}

public getIsMobileResolution(): boolean {
return this.isMobileResolution;
}
}

There are a few things to note here:

  • For the example, I am only dealing with two kinds of view: mobile and desktop. However, it can easily be converted to a few different views (for example, desktop, tablet and mobile, each with different width size) using an enum.
  • I am selecting the current view according to the resolution. I found it to be the most accurate way, but there are other options. For example, you can use the user-agent header which can tell which device is using the site.
  • I have chosen to separate the two views in the 768 px since this is iPad width, and therefore iPad and up will receive the desktop view and the rest (smaller resolution) will receive the mobile resolution. Of course, that other resolution barrier can be used — this is the designer’s call.
  • The window element is only available on a browser platform. For Angular Universal or any other server rendering, we will have to use another mechanism to determine the view, for example, a user-agent header.

Options 1 and 2: Different Views, Different Routes

Now that we have our infrastructure, we can continue in the implementation.
Let’s first deal with route differences between different views. There are a few reasons why routes can change between views:

  • If we need to use different components in different views
  • If we have a component which in one view requires its own route but in another view does not require a route or requires a different route — I also suggest avoiding this option for the same reasons that apply to option 2. above, under Motivation. But again, in some cases, it’s a must. As an example, in SeeVoov site, the user profile in the mobile view has its own route, while in the desktop view it’s a pop-up that can be seen in every route.
SeeVoov user profile — mobile vs. desktop
  • If the different views have the same routes but require some hierarchy changes such as router outlet

Back to our sample app, we will add a routing module and create two route arrays, one for the desktop view and one for the mobile view:

const desktop_routes: Routes = [
{
path: '', component: DesktopFrontpageComponent, children:
[
{path: 'product/:productName', component: ProductComponent}
]
},
{path: 'about', component: AboutComponent},
// directs all other routes to the main page
{path: '**', redirectTo: ''}
];

const mobile_routes: Routes = [
{path: '', component: MobileFrontpageComponent},
{path: 'product/:productName', component: ProductComponent},
{path: 'about', component: AboutComponent},
{path: 'user-profile', component: UserProfileComponent},
// directs all other routes to the main page
{path: '**', redirectTo: ''}
];

Let’s explore the above code.

As for the front page, we can see that we are using different components for the mobile and desktop as in option 2. In our small sample app, the logic difference between the desktop front page and mobile front page doesn't really justify making them two different components, but for the sake of the example, we will do it. As for the products pages, we can see that, in the desktop version, they are child routes of the front page, while in the mobile they are independent pages.

The products components will have different views to demonstrate option 3.
The About component will serve us to demonstrate option 1. (a responsive view). The user profile component is included only in the mobile routes. However, it will exist in the desktop version as well, only as a pop-up and not an independent route.

Using the routes above, we can demonstrate all the options we reviewed earlier.

So now that we have two sets of routes, how do we switch between them according to the current view resolution? The answer is the Router.resetConfig function. The resetConfig function resets the router routes to the given array. In our app, we can add this code in the routing module constructor:

@NgModule({
// as default we set the desktop routing configuration. if mobile will be started it will be replaced below.
// note that we must specify some routes here (not an empty array) otherwise the trick below doesn't work...
imports: [RouterModule.forRoot(desktop_routes, {preloadingStrategy: PreloadAllModules})],
exports: [RouterModule]
})
export class AppRoutingModule {

public constructor(private router: Router,
private applicationStateService: ApplicationStateService) {

if (applicationStateService.getIsMobileResolution()) {
router.resetConfig(mobile_routes);
}
}
}

Looks good! But there are a few things to note about it:

  • If you are loading a module in your router instead of a component (lazy loading, for example) the module probably brings routes with it. Now, the resetConfig function replaces all the router routes in the entire application, so you can’t just call it inside the child module and expect it to change only the child module routes. In fact, you can only call the resetConfig function in the root routing module. Otherwise, you will corrupt your routes config.
    This means that, for the child modules, you need to find a different way to switch between the views routes. (This is another reason I suggest avoiding having different routes config between the views, especially in child modules). One solution, for example, is to inject the new routes into the router current route recursively. I have created a function that does that in the sample GitHub project (under the routing module). But pay attention — it’s not tested enough and it’s not recommended to manipulate the routes manually. But it works :).
  • In Angular Universal, there is some bug that, after calling the resetConfig function, suddenly some CSS doesn’t arrive with the HTML.

Great, we passed the routes changes challenge! This covers options 1. and 2. (see the example app). Now we can continue to option 3., which obligates us to deal with multiple views for a single component.

Option 3: Single Component, Multiple Views

We want to be able to change the view of a component while using the same logic in run time according to the resolution. To achieve this we will use a good old software design pattern called MVC (model-view-controller). MVC is a software architectural pattern for implementing user interfaces. It divides a given application into three interconnected parts — model, view and controller. This is done to separate internal representations of information from the ways information is presented. The biggest advantage of this architecture is that it enables us to replace the view component shamelessly (that’s what we want!)

MVC Pattern

The model role is holding the information that needs to be presented to the user. The view role is to render the data currently inside the model to the display and notify the controller on user actions. The controller role is to update the model according to user actions and business logic. Once the controller finishes updating the model, it asks the view to update the display with the new data in the model.

Once the three parts of MVC are implemented, the flow is as follows.
The controller fills the model with the data that needs to be presented according to the business logic. Once the model is ready with the data, the controller commands the view to update itself. Upon this command, the view reads the new model data and renders it. If the user interacts with the view, the view notifies the controller on the user action, and the process starts over.

Actually, Angular is already a kind of MVC. If we will look at an Angular standard component, we see the following structure:

Angular standard component structure

It’s obvious that the view is the HTML and SCSS files. Also, it seems the .ts file is the controller since it holds all the logic. But where is the model? The answer is that the model resides implicitly inside the .ts file as well. Every time we bind an HTML control to a typescript member value, we bind the control to the model.

product.component.html:
Desktop View:
<h1 class="product-title">
{{name}}
</h1>

<img [src]="imageUrl" alt="{{name}} name">

<p>
{{description}}
</p>
...
--------------------------------------------------------------------
product.component.scss:
:host {
position: fixed;
top: 10vh;
right: 10vw;
bottom: 10vh;
left: 10vw;
padding: 25px;
background: rgba(255, 255, 255, 0.9);
border-radius: 10px;
}

img {
width: 25%;
float: left;
margin: 0 30px 30px 0;
}

:host ::ng-deep .mat-tab-label {
text-transform: uppercase;
}
--------------------------------------------------------------------
product.component.ts:
export class ProductComponent {

public name: string;
public description: string;
public imageUrl: SafeResourceUrl;

constructor(private router: Router,
private sanitizer: DomSanitizer) {
this.loadProduct();
}

private loadProduct(): void {
// fills the product parameters
...
}
}

We can change the component structure a bit to make the model more explicit:

Angular component structure with model

The model looks like this:

product.component.model.ts:
export class ProductComponentModel {
public name: string;
public description: string;
public imageUrl: SafeResourceUrl;

constructor(private sanitizer: DomSanitizer) {
this.name = '';
this.description = '';
this.imageUrl = this.sanitizer.bypassSecurityTrustResourceUrl('');
}
}

And the new controller looks like this:

product.component.ts:
export class ProductComponent {

private model: ProductComponentModel;

constructor(private router: Router,
private sanitizer: DomSanitizer) {
this.model = new ProductComponentModel(sanitizer);
this.loadProduct();
}

private loadProduct(): void {
// fills the product parameters inside the model
...
}

Let’s update the view (HTML file) accordingly.

product.component.html:
Desktop View:
<h1 class="product-title">
{{model.name}}
</h1>

<img [src]="model.imageUrl" alt="{{model.name}} name">

<p>
{{model.description}}
</p>
...

Great, now we have a perfect MVC! But something is still missing. As I explained before, the controller, which holds the view and model, also controls when the view re-renders the model to the display (using the UpdateView command). This means that, even if the controller changes the model, the view should not render the new changes until the controller commands the view: UpdateView.

The advantage of this approach is that in many cases it takes time to update the model. Let’s assume the controller sends few calls to servers and each call updates some member in the model. If we won’t use the UpdateView approach, whenever some request finishes, the view will present its result and then there is a chance the user will see an incomplete state (some data is updated, and some isn’t).

With the UpdateView approach, the controller updates the model whenever some request ends. Once all requests are completed, it calls the UpdateView command and only then the view renders the new model and the user receives a holistic picture of the data. As we know, Angular is using the zone to detect changes and renders the view automatically on every change. So how will we achieve the UpdateView approach? The answer is clone. let’s change our controller code as follows:

product.component.ts:
export class ProductComponent {

private model: ProductComponentModel;
public myViewModel: ProductComponentModel;

constructor(private router: Router,
private sanitizer: DomSanitizer) {
this.model = new ProductComponentModel(sanitizer);
this.myViewModel = new ProductComponentModel(sanitizer);
this.loadProduct();
}

private loadProduct(): void {
// fills the product parameters inside the model
...
}
}

And the view as follows:

product.component.html:
Desktop View:
<h1 class="product-title">
{{myViewModel.name}}
</h1>

<img [src]="myViewModel.imageUrl" alt="{{myViewModel.name}} name">

<p>
{{myViewModel.description}}
</p>
...

Now the view is bound to the instance myViewModel instead of to the model instance. When the controller updates the model, the display won’t change. All that’s left is to update the instance of myViewModel with the data inside the model every time the controller command UpdateView is used.

We will do that as follows:

product.component.model.ts:
export class ProductComponentModel {

public name: string;
public description: string;
public imageUrl: SafeResourceUrl;

constructor(private sanitizer: DomSanitizer) {
this.name = '';
this.description = '';
this.imageUrl = this.sanitizer.bypassSecurityTrustResourceUrl('');
}

public clone(): ProductComponentModel {
let clonedModel: ProductComponentModel = new ProductComponentModel(this.sanitizer);
clonedModel.name = this.name;
clonedModel.description = this.description;
clonedModel.imageUrl = this.imageUrl;
return clonedModel;
}
}
-------------------------------------------------------------------
product.component.ts:
export class ProductComponent {

private model: ProductComponentModel;
public myViewModel: ProductComponentModel;

constructor(private router: Router,
private sanitizer: DomSanitizer) {
this.model = new ProductComponentModel(sanitizer);
this.myViewModel = new ProductComponentModel(sanitizer);
this.loadProduct();
this.updateView();
}

private loadProduct(): void {
// fills the product parameters inside the model
...
}
private updateView(): void {
this.myViewModel = this.model.clone();
}
}

Now the controller can update the model as much as it wants, and the view will not get updated. The view will get updated only when the controller calls the function UpdateView.

Here are a few notes about this technique.

  • The clone function in the model must really clone the data. For example, if the model holds an array, it’s not enough just to do the following, because then only the array reference will be copied.
model.someArray = this.someArray;

The view will still be bounded to the array in the model. To clone an array we have to do this:

model.someArray = this.someArray.map(arrayElement=>{
clone array element});

This will create a new array with new elements inside it.

  • In continuation to the first bullet, nested objects should be cloned as well (as opposed to primitive types that can just be copied using =). For example, if our model holds a member whose type is UserDetails, UserDetails object should also include a clone function and in the root model clone function it should be called.
  • If the view includes a scrollable list that is bound to an array in the model, it may be problematic to clone the array. The reason is that a scrollable list has some state in the dom (its position) and if the array instance changes it corrupts the state. In these cases, the array elements should be updated without creating a new array.
  • I am using the name myViewModel for the member of the model which the view is bounded to and not viewModel since viewModel is a save keyword from some reason.
  • It’s important to initialize the model with empty values in the constructor and to create it empty in the controller constructor. Otherwise, Angular will complain on null values in the HTML when the component loads.

Now that we have designed our component according to MVC architecture, we are ready to replace the view. Since our view (the HTML) is bounded to a well-defined model, if we write a new view that is also bounded to the same model. Switching between them will be transparent. Moreover, we will be able to change our view without changing our business logic (written in the controller and modeled in the model).

Let’s add a mobile view:

product.component.mobile.html:
Mobile View:
<h3 class="product-title">
{{myViewModel.name}}
</h3>

<img [src]="myViewModel.imageUrl" alt="{{myViewModel.name}} name">

<p>
{{myViewModel.description}}
</p>
--------------------------------------------------------------------
product.component.mobile.scss:
img {
width: 100%;
margin-bottom: 15px;
}

As you see, we created the new view in a different SCSS and in different HTML files, which is the most elegant way. Now we want to select the correct view files on run time according to the resolution. To do so, we will use Objects inheritance (Thank god we have typescript :-) ). Let’s make a small refactor to our controller (which is the main component .ts file):

--------------------------------------------------------------------
product.component.ts:
export abstract class ProductComponent {

private model: ProductComponentModel;
public myViewModel: ProductComponentModel;

constructor(private router: Router,
private sanitizer: DomSanitizer,
private applicationStateService: ApplicationStateService) {
this.model = new ProductComponentModel(sanitizer);
this.myViewModel = new ProductComponentModel(sanitizer);

this.loadProduct();

this.updateView();
}
...}--------------------------------------------------------------------
product.component.desktop.ts:
@Component({
selector: 'app-product-desktop',
templateUrl: './product.component.desktop.html',
styleUrls: ['./product.component.desktop.scss']
})
export class ProductComponentDesktop extends ProductComponent {

constructor(router: Router,
sanitizer: DomSanitizer,
applicationStateService: ApplicationStateService) {
super(router, sanitizer, applicationStateService);
}

}
--------------------------------------------------------------------
product.component.mobile.ts:
@Component({
selector: 'app-product-mobile',
templateUrl: './product.component.mobile.html',
styleUrls: ['./product.component.mobile.scss']
})
export class ProductComponentMobile extends ProductComponent {

constructor(router: Router,
sanitizer: DomSanitizer,
applicationStateService: ApplicationStateService) {
super(router,sanitizer,applicationStateService);
}

}

So what happened? Don’t be alarmed — it’s not that difficult. What we did is that we changed the main component file (which is the controller for us) to an abstract class and removed the Angular component decorator. Then we created two new .ts files, one for the desktop and one for the mobile. Each extends the main component file and by that receives all the logic (including the model). On the specific component files, we add back the Angular component decorator. Now in each one, we reference the relevant SCSS and HTML files. Each specific component also receives its own selector name.

The above approach enables us to separate the two different views completely while saving the advantage of one controller with a single logic and one model.

All that is left is to load the correct view when the page loads according to the resolution. Since we separated our routes, it’s pretty easy:

--------------------------------------------------------------------
app.routing.module.ts:
const desktop_routes: Routes = [
{path: 'about', component: AboutComponent},
{
path: '', component: DesktopFrontpageComponent, children:
[
{path: ':productName', component: ProductComponentDesktop}
]
},
// directs all other routes to the main page
{path: '**', redirectTo: ''}
];

const mobile_routes: Routes = [
{path: '', component: MobileFrontpageComponent},
{path: 'about', component: AboutComponent},
{path: 'user-profile', component: UserProfileComponent},
{path: ':productName', component: ProductComponentMobile},
// directs all other routes to the main page
{path: '**', redirectTo: ''}
];

In case our component is not loaded by routes (implemented inside HTML) we have 2 options:

  1. If the father component (which references our component) is also separated to views as ours is, we can use the same technique and add the correct view selector in the correct view of the father component.
  2. If the father component has a single view (responsive, for example) we can refactor a bit the main abstract class of our component to have a view itself and inside it use ngIf to select the correct view according to the ViewKind parameter (isMobileResolution in the example below):
--------------------------------------------------------------------
product.component.ts:
@Component({
selector: 'app-product',
templateUrl: './product.component.html'
})
export class ProductComponent {

private model: ProductComponentModel;
public myViewModel: ProductComponentModel;

public isMobileResolution: boolean;

constructor(private router: Router,
private sanitizer: DomSanitizer,
private applicationStateService: ApplicationStateService) {

this.model = new ProductComponentModel(sanitizer);
this.myViewModel = new ProductComponentModel(sanitizer);

this.loadProduct();
this.updateView();

this.isMobileResolution = applicationStateService.getIsMobileResolution();
}
--------------------------------------------------------------------
product.component.html:
<div class="desktop-container" *ngIf="!isMobileResolution; else mobileContainer">
<app-product-desktop></app-product-desktop>
</div>

<ng-template #mobileContainer>
<app-product-mobile></app-product-mobile>
</ng-template>

Now we can reference the main component wherever we want, and it will load the correct view for us according to the resolution.

After all the hard work, we did it! Let’s run our app and see it in action:

example app product page — desktop view vs. mobile view

Here are a few notes on the above:

  • Switching between the different resolution views requires page refresh since we are setting the isMobile parameter in the constructor. It’s not a problem anyway, because our users will enter the site with the correct resolution from the beginning.
  • In some cases, a certain view requires an extended model over the controller base model. For example, the view holds a third party that requests to receive data in a predefined object. Since this third party is view-dependent, the controller is not aware of this structure and therefore it is not represented in the base model. In these cases, we can use inheritance again and create a model for the specific view which extends the base model. Then, using abstract methods and overriding, we create the correct model according to the view and update it respectively.
  • If the view also requires certain logic which is only relevant to that specific view (open drop-down upon user click) this logic can be added in the views component file without interfering with the controller.

That’s it — we’ve created a multiview Angular web app! I tried adding all the options listed above in the example app. You can find it here: https://github.com/yosigolan/angular-multi-view

Enjoy!

--

--

Responses (10)