The Angular Framework Forces You To Learn More JavaScript, Making You A Better JavaScript Programmer
CAUTION: This post is entirely tongue-in-cheek (ie, not meant to be taken seriously). The very idea that a framework can force you to learn more or less JavaScript is completely ludicrous. And, any engineer that suggests otherwise has clearly not yet had sufficient caffeine. The only thing that impacts the amount of JavaScript you learn with your choice of framework is YOU and your desire to learn. That's it.
Of all the client-side JavaScript frameworks today, I choose to use Angular, which just released version 6.0 at the time of this writing. Not only does Angular allow me to build powerful and robust enterprise-grade applications, it also forces me to learn much more about the JavaScript language itself. Some engineers would call Angular an opinionated framework. But, for the most part, it's just a powerful dependency-injection container with serious change-detection capabilities that gets out of your way. The rest is up to you to customize, organize, and develop as you see fit. Ultimately, Angular empowers you to make your own choices and helps you to become an even better JavaScript developer.
TypeScript Unleashes The Power of ES6 (and Beyond)
While TypeScript isn't strictly necessary for Angular development, it is both recommended and totally freakin' awesome. Not only does TypeScript allow you to leverage compile-time type safety and seamless dependency-injection semantics, it also let's you take advantage of all ES6 features today, regardless of cross-browser compatibility. This is part of why Angular makes you such a good JavaScript developer - it helps you stay ahead of the language curve by removing the fear of deploying non-compatible syntax.
For example, using TypeScript with Angular allows you to start jamming-out on the following ES6 (and beyond) features without any concern of client-side errors:
- Classes.
- Fat-arrow functions.
- Default function parameter values.
- Rest and spread operators.
- Template strings.
- Short-hand object literals.
- Dynamic object key syntax.
- Destructuring.
- Asynchronous control flow (ie, generators, yield, async, await).
- Block-scope variable declaration.
- For-Of iteration.
How exciting is that! It's like we're living in the future. In fact, just reading this list of feature that are enabled by the Angular framework (with TypeScript) may have just made you a better JavaScript developer because there may be features listed here (like For-Of iteration) that you didn't event know existed. The learning has already begun!
Goodbye CommonJS Modules, Hello ES6 Modules
With the power of TypeScript's transpilation, you can start to learn and leverage ES6 modules immediately. Gone are the days of having to use Node's proprietary "require" system. Now, you can dive into ES6 and start using the latest and greatest import and export mechanics. For example, we can create a file that exports two individually named values:
// With ES6 module syntax, we can now used named exports! Notice that I am exporting
// two values - a Function and a Class (which is really just a Function with a more
// fascinating prototype chain).
export function createThing() : Thing {
return( new Thing() );
}
export class Thing {
public toString() : string {
return( "I am a thing!" );
}
}
Then, we can import those named values into a consuming context:
// With ES6 module syntax, we can now import the named exports from the target file. We
// can even rename values as we import them!
// --
// NOTE: I am using the "import as" syntax just to demonstrate the flexibility.
import { createThing } from "./thing";
import { Thing as ThingType } from "./thing";
var thing: ThingType = createThing();
console.log( thing.toString() );
Of course, some of the minor details here are TypeScript specific, such as denoting methods as "public" and defining their return types. But, the export / import usage is pure ES6 module syntax. And, by using Angular, you're fast on your way to becoming a pro at designing JavaScript modules.
ES6 Classes For the Win
In the previous section, one of the values that we exported from our ES6 module was an ES6 Class. Thanks to TypeScript's transpilation, we can safely use ES6 Classes as much as we want. And, in Angular, we want to use Classes a lot. They lay the foundation for our Type system and make dependency-injection insanely delicious. By choosing the Angular framework, you're going to get a lot of practice designing and implementing abstractions using ES6 Classes. For example, it's quite common to create ES6 Classes for data-access:
export interface User {
id: number;
name: string;
}
export class UserGateway {
public getByID( id: number ) : Promise<User> {
var promise = new Promise<User>(
( resolve, reject ) : void => {
resolve( this.generateUser() );
}
);
return( promise );
}
// ---
// PRIVATE METHODS.
// ---
private generateUser() : User {
var names = [ "Sarah", "Jo", "Tricia", "Anna" ];
var id = Math.floor( Math.random() * 100 );
var name = names[ Math.floor( Math.random() * names.length ) ];
return({ id, name });
}
}
Here, we're just randomly generating a value for our "user" object; but, it's not hard to imagine replacing the random value with some sort of local database like PouchDB or a remote API accessed via AJAX (Asynchronous JavaScript and JSON).
But wait, we can do even better than this clunky Promise syntax. By choosing Angular and TypeScript, you can stay on the bleeding edge of JavaScript's language syntax. I'm talking about asynchronous control flow features like async and await. We can rewrite this ES6 Class to use friendlier Promise-driven syntax:
export class UserGateway {
public async getByID( id: number ) : Promise<User> {
return( this.generateUser() );
}
// ---
// PRIVATE METHODS.
// ---
private generateUser() : User {
var names = [ "Sarah", "Jo", "Tricia", "Anna" ];
return({
id: Math.floor( Math.random() * 100 ),
name: names[ Math.floor( Math.random() * names.length ) ]
});
}
}
Notice that we've replaced the entire new Promise() constructor with the "async" keyword. This is much nicer to read. And, it improves the error handling since synchronous errors will implicitly be caught and parled into Promise rejections.
By choosing Angular, you're well on your way to mastering ES6 Classes and becoming an expert wrangler of asynchronous control flows!
Functional Programming Is The New Hawtness In JavaScript
Now, I know what you're thinking: Angular might make me an expert at using ES6 Classes; but, aren't Classes old-skool? Isn't Functional Programming the new hawtness? Aren't all the cool kids partially-applying functions and getting Curry in a hurry?
Personally, I love using Classes - the dependency-injection alone makes them a no-brainer - an immediate boost to productivity (above and beyond the obvious benefits of encapsulation). But, the good news is, you can pretty much do whatever you want. I do believe that some constructs, like Directives and Pipes, do need to be classes - at least when using TypeScript; but, essentially every "service" that you design is completely up to you. Like I said earlier, Angular is fairly unopinionated. So, if you want to flex your Functional muscles, Angular will gladly get out of your way.
For example, since Angular promotes the ES6 module syntax, you can already leverage all of the most popular Functional Programming libraries that you're used to using. Just import the functions you need and you're ready to rock:
// With Angular and TypeScript, we can import Functional Programming methods just like we
// would any other type of ES6 module. However, modules that export a single value, such
// as is common in CommonJS libs, we use a slightly different "import = require" syntax.
import get = require( "lodash/fp/get" );
var friends = [
{ id: 1, name: "Joanna" },
{ id: 2, name: "Kimmie" },
{ id: 3, name: "Anna" }
];
console.log( friends.map( get( "name" ) ) ); // ["Joanna", "Kimmie", "Anna"]
In this case, the Lodash exports the "get" function as the "module.exports" value. As such, we have to use a slightly modified "import = require" syntax. However, once we have the imported value, you can use it just as you would any other import.
Personally, I don't really enjoy reading Functional Programming code. I find that it requires too many mental gymnastics, trying to keep a model in my head on how one things feeds into another thing. As such, I prefer Class-based behaviors. But, that's just me and my personal preference. And, that's the beauty of Angular - you can still do things the way you want to do them. Angular might be giving you rocket boosters; but, you're still the one in control - you still get to steer the ship.
All of the Array Methods!
For some reason, in the shadow of the rising star of Functional Programming, many people believe that you're not a real JavaScript ninja rockstar unicorn 10x'er until you know all of the Array methods. Well here's the good news: when you choose Angular as your JavaScript framework, you get to use all of the Array methods! And, I mean all of them!
The use of Array methods is so prevalent in Angular, it's hard to pick just one example. But, imagine creating a data-access object around an in-memory data-store. Accessors for that data could very reasonably be powered by array methods that operate on the internal cache:
interface Friend {
id: number;
name: string;
age: number;
}
export class FriendGateway {
private friends: Friend[];
constructor() {
this.friends = [];
}
// ---
// PUBLIC METHODS.
// ---
public addFriend( friend: Friend ) : void {
this.friends.push({
id: friend.id,
name: friend.name,
age: friend.age
});
}
public deleteFriend( id: number ) : void {
var index = this.friends.findIndex(
( friend ) : boolean => {
return( friend.id === id );
}
);
if ( index >= 0 ) {
this.friends.splice( index, 1 );
}
}
public getAverageAge() : number {
var totalAge = this.friends.reduce(
( total, friend ) : number => {
return( total + friend.age );
},
0
);
return( totalAge / this.friends.length );
}
public getFriend( id: number ) : Friend {
var friend = this.friends.find(
( friend ) : boolean => {
return( friend.id === id );
}
);
return( friend || null );
}
public getFriends() : Friend[] {
var friends = this.friends.map(
( friend ) : Friend => {
return({ ...friend });
}
);
return( friends );
}
}
In this case, I'm using an internal cache that's mutable; but, I'm copying objects as they enter and leave the ES6 Class boundary. That said, you could just have easily used something like Immutable.js in order to ensure that object references don't leak out of the abstraction.
Now, I have to admit that I actually lied to you. I said that, with Angular, you get to use all of the Array methods. Well, that's not exactly true. With Angular and TypeScript you actually get to use moar than all of the Array methods! With a feature known as "Module Augmentation", you can actually access both the Array constructor and the Array prototype and add both static and instance methods, respectively.
For example, right now, the Array class doesn't have a method that both filters and maps at the same time (much like jQuery's .map() method does or the canonical flatMap method might). But, that's no problem - we can add that method ourselves, complete with type-safety:
// Define the "filter map" operator.
interface Operator<T, U> {
( value: T, index: number, values: T[] ) : U;
}
// If we want to augment one of the global / native modules, like Array, we have to use
// the special "global" module reference.
declare global {
// If we want to add INSTANCE METHODS to one of the native classes, we have
// to "declaration merge" an interface into the existing class.
interface Array<T> {
filterMap<T, U>( operator: Operator<T, U>, context?: any ) : U[];
}
}
// I map the contextual collection onto another collection by appending defined results
// of the operation onto the mapped collection. This means that "undefined" results will
// be omitted from the mapped collection.
// --
// CAUTION: Augmentations for the global scope can only be directly nested in external
// modules or ambient module declarations. As such, we are EXPORTING this function to
// force this file to become an "external module" (one that imports or exports other
// modules).
export function filterMap<T, U>( operator: Operator<T, U>, context: any = null ) : U[] {
var results = this.reduce(
( reduction: U[], value: T, index: number, values: T[] ) : U[] => {
var mappedValue = operator.call( context, value, index, values );
if ( mappedValue !== undefined ) {
reduction.push( mappedValue );
}
return( reduction );
},
[]
);
return( results );
};
// Protect against conflicting definitions. Since each module in Node.js is evaluated
// only once (and then cached in memory), we should never hit this line more than once
// (no matter how many times we include it). As such, the only way this method would
// already be defined is if another module has injected it without knowing that this
// module would follow-suit.
if ( Array.prototype.filterMap ) {
throw( new Error( "Array.prototype.filterMap is already defined - overriding it will be dangerous." ) );
}
// Augment the global Array prototype (ie, add an instance method).
Array.prototype.filterMap = filterMap;
Here, we're telling TypeScript that the global definition for Array should contain a new instance method - filterMap() - which we add to the Array's prototype chain. Then, from any other context, we can include that module augmentation and leverage the .filterMap() method in our array manipulation logic:
// Since we are augmenting the native Array module, we have to include this import
// in any page that needs to know about the enhanced Array definition.
import "./array-filter-map";
interface Friend {
id: number;
name: string;
}
var friends: Friend[] = [
{ id: 1, name: "Sarah" },
{ id: 2, name: "Joanna" },
{ id: 3, name: "Tricia" }
];
// Notice that we are using the augmented .filterMap() method.
var someFriends = friends.filterMap(
( friend: Friend ) : string => {
// Exclude this friend from the mapping.
if ( friend.id === 2 ) {
return;
}
return( friend.name );
}
);
console.log( someFriends ); // ["Sarah", "Tricia"]
That's a very interesting approach. But, dynamically changing class Prototypes is "overly clever" and is not recommended. Instead, the community is moving in a more "Functional Programming" (FP) direction that "applies" operators over a collection. This FP approach also facilitates better dead-code elimination and is exemplified in libraries like RxJS and its .pipe() handler.
The point I'm trying to make, however, is that with Angular, you're use of Array methods is gonna be thrust into overdrive. Before you know it, you're gonna be mapping, reducing, filtering, and iterating your way to much victory!
ES6 Fat Arrow Functions as Class Methods
As we've seen in the previous sections, ES6 Fat Arrow functions are everywhere in Angular. In fact, they are used in places that they don't even need to be; but, they're used because of the delicious syntactic sugar that they provide. The beautiful thing about using Angular with TypeScript is that you actually get to use ES6 Fat Arrow functions as part of ES6 Class definitions! Now, you can easily bind methods to classes without having to litter your code with archaic .bind() calls:
export class MyComponent {
constructor() {
document.addEventListener( "mousedown", this.handleMousdown, false )
}
public handleMousdown = ( event: MouseEvent ) : void => {
console.log( `Click at { ${ event.clientX } , ${ event.clientY } }` );
}
}
Notice that our ES6 Class definition has a public method named "handleMousedown". But, if you look closely, you'll actually see that this is not a method declaration; instead, it's a property assignment. And, the value being assigned is an ES6 Fat Arrow function. This approach creates a new handleMousedown method binding for each instance of the ES6 Class, allowing you to pass around the "this.handleMousedown" references without losing the expected "this" execution context.
When you choose Angular as your JavaScript framework, ES6 Fat Arrow functions are going to become second nature to you. If you get cut, you're gonna bleed Fat Arrow functions.
The Browser's Native Event System
While not part of the core JavaScript specification, one of the great things about the Angular framework is that it's built on top of the Browser's native Event system. Other frameworks create a "synthetic event system" that doesn't adhere to expected Event behavior. As such, in those frameworks, you have to learn "their way" of dealing with propagation and default behavior alteration. With Angular, that's not the case. With Angular, you'll build a robust understanding of how the Browser deals with Events. And, you can do things like stopping Event propagation at any level of the Document Object Model (DOM) tree.
So, not only will Angular make you a better JavaScript programmer, it will also help you become more intimately aware of how the Browser manages the DOM and the event propagation within that DOM.
Can We Please Talk About The Massive Benefits of Dependency-Injection (DI)
No conversation about Angular is complete without talking about Dependency-Injection, or "DI". Angular applications are all built on the concept of Dependency-Injection. TypeScript is not required for dependency-injection; but, TypeScript certainly makes dependency-injection easier to understand and consume within an Angular application.
Dependency-Injection is the act of "inverting control". Which means that instead of one object controlling which references it knows about, it defers to a calling context to provide references. For example, if an application uses a "Store" for client-side data caching, a tightly-coupled application might simply require that Store wherever it is needed:
var myStore = require( "./stores/my-store" );
In this case, the contextual file is "controlling references" by explicitly requiring them into the current context. This creates tight coupling between the current file and the given Store implementation. This tight coupling makes for brittle code that is harder to change and, certainly, harder to test.
With the Dependency-Injection in an Angular application, we can invert that control by providing the Store implementation to the given file:
import { MyStore } from "./stores/my-store";
export class MyComponent {
// Here, we're telling Angular that we want this constructor to receive an
// implementation of the "MyStore" Type. In production, this "MyStore" instance may
// be something that writes to LocalStorage. And, in testing, this "MyStore" instance
// may be something that simply cache in-memory. The Inversion of Control and the
// subsequent loose-coupling allows us to change the implementation details
constructor( myStore: MyStore ) {
console.log( "Injected dependency:", myStore )
}
}
Here, we're "coding to an interface". This means that we're telling Angular that we want something that implements the MyStore API; but, we don't couple the contextual ES6 Class to any particular implementation of the MyStore API. This allows us to swap the MyStore implementation in and out as needed (such as when performing unit tests or fleshing out a new idea).
So, while Dependency-Injection doesn't specifically impact your understanding of the JavaScript language, Dependency-Injection and Angular will absolutely make you a better JavaScript developer by helping you to create better boundaries and separations of concern. If you've heard of Uncle Bob's "SOLID Principles", they are essentially all about Dependency-Injection.
I hope you can see how amazing Angular is. And, how choosing Angular as your framework will make you both a better JavaScript developer and a better application architect. Not only does Angular and TypeScript empower to you use cutting-edge ES6 (and beyond) syntax, the core concepts of Dependency-Injection will super-charge your separation of concerns, clean lines, and decoupling of features.
Strap on that rocket-pack, baby! It's time to fly!
Want to use code from this post? Check out the license.
Reader Comments
Ben. Another great post!
Because I was running into some circular dependency issues, the other day, I decided to try out an implementation that I use when creating custom DI in my CF component frameworks.
So, I wondered whether this approach would work in Angular. And, to my delight, it does!
In fact, this is why I like Angular, because there are so many parallels with CF. Even a lot of the terminology is similar. Angular & CF make a great team!
Basically, I use an explicit setter & getter method in the Angular Component with the offending circular dependency , and I am good to go. Then I just set the service in my app.component.ts:
app.component.ts
import { FooService } from './foo/foo.service';
import { BarService } from './bar/bar.service';
export class AppComponent {
constructor(public fooService: FooService, public barService: BarService) {
this.fooService.setBarService(barService);
}
}
foo.service.ts
@Injectable()
export class FooService {
barService: any;
constructor(){
}
setBarService(barService: any): void {
this.barService = barService;
}
getBarService(): any {
return this.barService;
}
}
@Charles,
Angular is just great, right? That said, let me take your idea and offer a slightly different approach (well, same approach, just in a different place). When your application is boot-strapped, the NgModule class constructor can actually work like "run" block for the application. Meaning, the NgModule constructor will run when your application is bootstrapped, after your services are instantiated, but before any of your components are created. As such, you could move the circular-injection into the NgModule constructor:
@NgModule({ .... })
export class AppModule {
. . . . constructor( foo: FooService, bar: BarService ) {
. . . . . . . . foo.setBarService( bar );
. . . . . . . . bar.setFooService( foo );
. . . . }
}
Then, by the time your AppComponent (or other components) need to inject FooService or BarService, the two services have _already been_ cross injected.
So, the same exact thing you are doing -- just in a slightly different place :)
Man, I really need to get something working for code-formatting in this blog. Been on my backlog for _ever_. Gotta carve out some time!
This is a cool improvement to my approach. Thanks for the heads up.
It is neater to have this kind of injection in the app.module.ts!
Yes. I wanted to say something about the code formatting thing.
And while we're at it, and this is me, being very spoilt, but on my mobile, your code blocks in your main blog post get cut off on the right hand side. Yes, I can scroll horizontally, but it makes it hard to follow to the end of each line!
I have seen blogs where, the code fits into an average width mobile screen. Granted the text looks a little bit smaller, but unless the majority of users have serious sight defects, I reckon it is acceptable.
@Charles,
Totally agree. Like a year ago, I tried to make the site mobile friendly (mostly because Google Analytics was going to start punishing me if I didn't). But, to be honest, I basically put in the minimum amount of effort. I have a whole list of things that I need to get working properly. The age of my blogging platform is really beginning to show. Time to shoot it full of Botox and Collagen and restore its youthful beauty :D
For me, blog content is way more important than the UI, but, I guess a few tweaks would be very welcome. I must admit that I read 99% of blog content in my mobile, nowadays. The digital landscape is changing fast. Maybe too fast for developers who have to continually update & upgrade. I have just updated all my iOS development tools, which was scary, and now I have to decide whether to learn Swift, after 5 years of Objective-C development. And then, whether to upgrade from Angular 5 => 6. And then there is my Lucee VPS, which is currently on version 4+. And CF11 => 2016
Maybe they will create an upgrades robot:)
The list is never ending...
@Charles,
Ha ha, and it sounds like you're actually making good progress! This blog still runs on ColdFusion 10 :( And, much of my day-to-day work life still runs on AngularJS 1.2.22. Talk about retro!! I'm trying hard to keep things moving forward, but it's an uphill battle when the skills I need at work don't overlap all that well with the skills I use in my "free time."
Baby steps....
Actually, this highlights a great point. Many applications work perfectly well using older tech. None of your readers actually know whether your blog runs on CF10 or CF2016, except for me, now ;)
Problems only occur with vendors that control both the software & hardware, like Apple, who can basically force you to upgrade.
This is why I am considering whether to abandon my iOS development work and go fully web based, with CF & Angular. I just love this partnership...
Very useful post. Keep sharing this kind of useful information