Make use of type unions and intersections. The TypeScript doc here explains how to combine types to make advance stuff work easily with the TS compiler. This comes extremely in handy when dealing with data from a RESTful API. Take a look at this example:
Here the ‘createdDate’ field can either be a JS Date, or a string. This is useful, because when the server provides you with an instance of a User, it will definitely send you the date in a string, but you may have a datepicker control which returns the date as a valid JS Date object, and to avoid misunderstanding your interface should know there are several options available.
Restrict your types. In TypeScript, you can even restrict the values that a field or a variable can have, like this:
This actually becomes a flag. If you have a variable of type Order, you can assign only one of these three string to the ‘status’ field, for any other string the TS compiler will throw an error.
You can do the same thing with enumerations:
Consider setting “noImplicitAny”: true. In the tsconfig.json file of your app you can set a flag which tells the compiler to throw errors when types are not explicitly provided. Otherwise the compiler assumes that a variable, whose type it cannot deduce, is of type any. That is not really the case, and though setting this flag to true may result in some unexpected complexities (will talk about this in another article), it mostly makes your code better.
Strictly typed code is less error-prone (bet you keep hearing that all the time!), and if TS provides you that, you should use it.
Components
Components are Angular’s core feature, and if you manage to have them neatly organized and reusable, you can consider half of your work done.
Consider having a (or even several!) base component classes. This may come in handy if you have lots of reused stuff (especially in templates!), and you just don’t want to pollute each component with the same code all over. Imagine a situation: you have several pages displaying some kind of system notifications. Each notification has a status (read/unread), and you, of course, have an enumeration with those status codes. And in every place a notification is displayed in your template you use ngClass to style the unread notifications. Now, you want to compare a notification’s status to a enumeration value rather than a hardcoded one, so you have to import the enumeration to your component:
So here we add an ‘unread’ class to every HTML element, which contains an unread notification. Notice how we created a ‘statuses’ field on the component class, so that we can use this enumeration in the template. But what if we use this enumeration in lots of components? Or what if we have other enumerations also used in different components? Should we keep creating (semantically) useless fields just to contain them? Seems like a lot of repeated code. Take a look at this:
So, now we have a BaseComponent (which is actually just a container!), from which our components can be derived to reuse application-global values and methods.
Another situation can be found often when working with forms. If you have an Angular form in your component, you may have fields and methods like this:
Of course you are going to have lots of components using Angular forms, so it may be nice to move this logic to a base class… but you don’t need this in your AbstractBaseComponent, because not every component has forms. But it would be nice to do it this way:
Now we have a separate class for components that use forms (notice how AbstractFormComponent extends AbstractBaseComponent, so we do not lose application-wide values). This is a nice shortcut and can be widely reused in places that really need it.
Container components. This one can be somewhat controversial, but you may still consider it to find out whether it suits you. You all know that a route maps to an Angular Component, but I suggest you use a container component, which will handle the data processing (if there is any) and pass the data downwards to another component, which will contain the actual view and further UI logic, using Inputs. Here is an example:
Here the container performs the data retrieval (it may perform some other common tasks too) and delegates the actual work to another component. This may come in handy when you have to reuse the same UI, but with existing data again, and is a good example of separation of concerns.
Small rule of thumb: whenever you write an ngFor directive on an HTML element with children, consider separating that element into a dependent component, like this:
This makes for lesser code in the parent component and allows to delegate any repeating logic to the child components.
Services
Services are the Angular solution to business logic placement and data handling. It is important to have well-structured services which provide access to data, data manipulation and other reusable logic. So here are some rules you should consider following:
Have a base service class for API calls. Put the simple HTTP service logic inside a base class and derive your API services from it. The base service may look like this:
Of course you can (and probably should) make this more complex, but the usage will still be simple as that:
Now you just abstract the API call logic away to the base class and can now focus on what data you are going to receive and how you will handle it.
Consider having (a) Utilities Service(s). Sometimes you will find out that you have some methods on your components used to deal with some data, maybe preprocess it or mutate in some way. Examples may be multiple, for an instance, you may have a file upload functionality in one of your components, so you need to convert an Array of JS File object to a FormData instance to perform an upload. Now this is not representational logic and will not in any way affect your view, and you may have several components containing a file upload, so consider creating a Utility or DataHelper service and moving this kind of functionality there.
Use TypeScript string Enums for API urls. TypeScript 2.4.1 is already out, so you you can make use of some benefits it carries with it, string enums being one of them.
Your app can interact with different API endpoints, so rather than hardcode them inside your methods, you may want to move them to string enums like this:
This provides a better insight into how your API works and what endpoint it has.
Consider caching your request results whenever possible. Rx.js allows you to cache the results of an HTTP Request (actually, any Observable, but now we just need the HTTP stuff), and there are examples where you may want to use that. For example, your API provides a endpoint which returns a JSON Array of Country objects, which you use in your application to render some lists or dropdowns to allow users to select countries from them. Of course, countries are not about to change every day, so the best course is to retrieve that data once the app needs it, cache it and then used the cached version during the app’s lifetime, rather than making an API call each time you need that list. Observables make this one extremely easy:
So now, whenever you subscribe to this country list, the result will be cached and you won’t need to make another HTTP Request in the future.
Templates
Angular uses html templates (of course, enriched with components, directives and pipes) to render the view of your application, so writing templates is inevitable, and keeping them neat and understandable is of paramount importance.
Delegate harder than primitive logic from templates to component methods. Notice how I used the phrase ‘harder than primitive’ instead of ‘complex’. This is because any logic other than checking straightforward conditions should be written in the component’s class method, not in the template directly. It is, writing ‘*ngIf=”someVariable === 1” ’ in your template is OK, anything longer should be moved away.
For example, you want to add a ‘has-error’ class to all form controls which are not properly filled in (not all validations have been successful). You can do this:
Just look how ugly that ngClass statement looks. It clutters the view and will create a lot of stupid repeating logic if we have more form controls. But, you can also do this:
Now we have just a nice piece of template, and can even easily test whether our validations work correctly with unit tests, without diving into the view.
You may notice I didn’t write anything yet about Directives and Pipes, so, that is because I want to write an article on how to work with DOM in Angular in more details, so this one will just mostly guide you through the ‘TypeScript’part of an Angular app.
Hope this helps you make your code cleaner and your app more structured. And remember, whatever you decide, be consistent.
No comments:
Post a Comment