Angular is not Java Part 4!

How to clean up polluted constructor in Angular

Kamil Konopka
7 min readJan 6, 2024
Photo by Emile Perron on Unsplash

Hopefully you’re able to read one of my previous articles:

Angular is not Java pt1!” as well as “Angular is not Java pt2!” and “Angular is not Java pt3!”. If not I strongly suggest to start right there to catch up:

Within this article will focus on some general miss-understanding of the “constructor” usage.

In general “constructor” within Javascript / Typescript is just syntactical sugar. Within Angular up until the version 16 constructor was used to provide Dependency Injection mechanism. It still is being used for DI, but also “Injection Context” mechanism was introduced as an alternative (If you’re interested in this topic, please read some of my previous articles ).

If your application is using prior version of Angular than 16, “constructor” should be reserved for DI only. Even if you’ll have a closer / deeper look into the original documentation, you’ll find this exact statement.

From clean code perspective / readability / maintainability it has a lot of sense. Sound obvious right? But, believe me or no, there are loads of projects where constructor calls are heavily overloaded with methods executions as well as attribute assignments.

As an example, sharing the slice of component’s code, I’ve found when was consulting one of the projects in the past. As the entire component contained almost 500! lines of code (sounds incredible right??) I’ve decided not too share everything, just bare minimum:

@Component({
selector: 'app-dictionary-details',
templateUrl: './dictionary-details.component.html',
styleUrls: [ './dictionary-details.component.scss' ]
})
class TestComponent {
// ... some other attributes
private sub = new Subscription();
formHelper: StateManager;
owningUnits$: Observable<Entry[]>;
standardizationDictionaryNames$: Observable<Entry[]>;
private dictionary$ = new BehaviorSubject(new Dictionary());
form: FormGroup;
public displayedColumnsEntries = DictionaryDetailsComponent.VIEW_MODE_DETAIL_ENTRIES_COLUMNS;
public displayedColumnsEntities = DictionaryDetailsComponent.VIEW_MODE_DETAIL_ENTITIES_COLUMNS;

canBeCreated: () => boolean;
canBeUpdated: () => boolean;
canBeCreatedOrUpdated: () => boolean;

constructor(private dictionaryService: DictionaryService,
private dictionaryReaderService: QatDictionaryReaderService,
private securityContext: SecurityContext,
private formBuilder: FormBuilder,
private router: Router,
private route: ActivatedRoute,
private toastr: ToastrService) {
this.entitiesForm = this.formBuilder.group({
displayName: [ '' ],
type: [ '' ],
owner: [ '' ]
}, {validators: atLeastOne(Validators.required, [ 'displayName', 'type', 'owner' ])});

this.setupForm();

this.sub.add(this.securityContext.userChanges.subscribe(user => {
if (user) {
this.canBeCreated = ((entitlement: Entitlement) => () =>
entitlement && entitlement.organizationalUnits.includes(this.form.get('owner').value)
)(_.first(this.securityContext.findMatchingEntitlements('std.dictionary.create')));
this.canBeUpdated = ((entitlement: Entitlement) => () =>
entitlement && entitlement.organizationalUnits.includes(this.form.get('owner').value)
)(_.first(this.securityContext.findMatchingEntitlements('std.dictionary.update')));
this.canBeCreatedOrUpdated = ((entitlements: Entitlement[]) => () => entitlements.length > 0
)(this.securityContext.findMatchingEntitlements(['std.dictionary.create', 'std.dictionary.update']));
}
}));

this.formHelper = new StateManager();
this.formHelper.modeObservable.pipe(filter(state => state.isAnyEditing))
.subscribe(state => {
if (this.dictionary.entries != null) {
this.displayedColumnsEntries = DictionaryDetailsComponent.EDIT_MODE_DETAIL_ENTRIES_COLUMNS;
} else {
this.displayedColumnsEntities = DictionaryDetailsComponent.EDIT_MODE_DETAIL_ENTITIES_COLUMNS;
}
this.form.enable();
if (state.action === Action.EDIT) {
this.form.get('name').disable();
this.form.get('owner').disable();
if (this.dictionary.entities.length > 0) {
this.disableColumns();
}
}
});

this.formHelper.modeObservable.pipe(filter(state => state.isViewing))
.subscribe(state => {
if (this.dictionary.entries.length > 0) {
this.displayedColumnsEntries = DictionaryDetailsComponent.VIEW_MODE_DETAIL_ENTRIES_COLUMNS;
this.removeLastEmptyEntry();
} else {
this.displayedColumnsEntities = DictionaryDetailsComponent.VIEW_MODE_DETAIL_ENTITIES_COLUMNS;
}
this.form.disable();
});

this.formHelper.modeObservable.pipe(filter(state => state.action === Action.CANCEL))
.subscribe(state => {
this.resetMainForm();
this.form.disable();
if (this.formHelper.get().fromState === State.EDITING_NEW) {
this.router.navigate([ '../' ], {relativeTo: this.route});
}
});

this.formHelper.modeObservable.pipe(filter(state => state.action === Action.SAVE))
.subscribe(state => {
this.resetMainForm();
this.form.disable();
this.toastr.success('dictionary has been saved', DefaultTitle.SUCCESS, {
timeOut: 3000,
closeButton: false
});
});

this.setupEntriesDragging();

this.owningUnits$ = combineLatest(
this.dictionaryReaderService.getByName(QatDictionaryName.OWNING_ORGANIZATIONAL_UNIT).pipe(
map(dict => dict && dict.entries || [])
),
this.dictionary$
).pipe(
map(([ entries, dictionary ]) => {
const units = this.securityContext.organizationalUnitsFor(this.formHelper.isEditingNew ? 'std.dictionary.create' : 'std.dictionary.update');
return entries.filter(e => (e.valid && units.includes(e.code)) || e.code === get(dictionary, 'owner'));
})
);

this.standardizationDictionaryNames$ = this.dictionaryReaderService.getByName(QatDictionaryName.STANDARDIZATION_DICTIONARY).pipe(
map(dict => dict && dict.entries || [])
);
}

// ... some other class methods
}

What’s interesting, when I’ve pasted the code above into the medium article section, automatic programming language recogniser picked it up as “Kotlin” :-)

If you take a look at the code, you’ll notice that there’s significantly more problems then just polluting the constructor. But will get there. I promise.

Let us think how we could make this component a little bit easier to read with cleaning up the constructor call entirely.

First step would be, to move attributes assignments from constructor into the class body as the result will be exactly the same as assigning within the constructor, but will be definitely easier to read. Also unifying the modifier would help as well:

@Component({
selector: 'app-dictionary-details',
templateUrl: './dictionary-details.component.html',
styleUrls: [ './dictionary-details.component.scss' ]
})
class TestComponent {
// ... some other attributes
private sub = new Subscription();
private dictionary$ = new BehaviorSubject(new Dictionary());

formHelper: StateManager = new StateManager();

owningUnits$: Observable<Entry[]> = combineLatest([
this.dictionaryReaderService.getByName(QatDictionaryName.OWNING_ORGANIZATIONAL_UNIT).pipe(
map(dict => dict && dict.entries || [])
),
this.dictionary$
]).pipe(
map(([entries, dictionary]) => {
const units = this.securityContext.organizationalUnitsFor(this.formHelper.isEditingNew ? 'std.dictionary.create' : 'std.dictionary.update');
return entries.filter(e => (e.valid && units.includes(e.code)) || e.code === get(dictionary, 'owner'));
})
);

standardizationDictionaryNames$: Observable<Entry[]> = this.dictionaryReaderService
.getByName(QatDictionaryName.STANDARDIZATION_DICTIONARY)
.pipe(map(dict => dict && dict.entries || []));

form: FormGroup = this.formBuilder.group({
displayName: [''],
type: [''],
owner: ['']
}, { validators: atLeastOne(Validators.required, ['displayName', 'type', 'owner'])});

displayedColumnsEntries = DictionaryDetailsComponent.VIEW_MODE_DETAIL_ENTRIES_COLUMNS;
displayedColumnsEntities = DictionaryDetailsComponent.VIEW_MODE_DETAIL_ENTITIES_COLUMNS;

canBeCreated: () => boolean;
canBeUpdated: () => boolean;
canBeCreatedOrUpdated: () => boolean;

constructor(private dictionaryService: DictionaryService,
private dictionaryReaderService: QatDictionaryReaderService,
private securityContext: SecurityContext,
private formBuilder: FormBuilder,
private router: Router,
private route: ActivatedRoute,
private toastr: ToastrService) {
this.setupForm();

this.sub.add(this.securityContext.userChanges.subscribe(user => {
if (user) {
this.canBeCreated = ((entitlement: Entitlement) => () =>
entitlement && entitlement.organizationalUnits.includes(this.form.get('owner').value)
)(_.first(this.securityContext.findMatchingEntitlements('std.dictionary.create')));
this.canBeUpdated = ((entitlement: Entitlement) => () =>
entitlement && entitlement.organizationalUnits.includes(this.form.get('owner').value)
)(_.first(this.securityContext.findMatchingEntitlements('std.dictionary.update')));
this.canBeCreatedOrUpdated = ((entitlements: Entitlement[]) => () => entitlements.length > 0
)(this.securityContext.findMatchingEntitlements(['std.dictionary.create', 'std.dictionary.update']));
}
}));

this.formHelper.modeObservable.pipe(filter(state => state.isAnyEditing))
.subscribe(state => {
if (this.dictionary.entries != null) {
this.displayedColumnsEntries = DictionaryDetailsComponent.EDIT_MODE_DETAIL_ENTRIES_COLUMNS;
} else {
this.displayedColumnsEntities = DictionaryDetailsComponent.EDIT_MODE_DETAIL_ENTITIES_COLUMNS;
}
this.form.enable();
if (state.action === Action.EDIT) {
this.form.get('name').disable();
this.form.get('owner').disable();
if (this.dictionary.entities.length > 0) {
this.disableColumns();
}
}
});

this.formHelper.modeObservable.pipe(filter(state => state.isViewing))
.subscribe(state => {
if (this.dictionary.entries.length > 0) {
this.displayedColumnsEntries = DictionaryDetailsComponent.VIEW_MODE_DETAIL_ENTRIES_COLUMNS;
this.removeLastEmptyEntry();
} else {
this.displayedColumnsEntities = DictionaryDetailsComponent.VIEW_MODE_DETAIL_ENTITIES_COLUMNS;
}
this.form.disable();
});

this.formHelper.modeObservable.pipe(filter(state => state.action === Action.CANCEL))
.subscribe(state => {
this.resetMainForm();
this.form.disable();
if (this.formHelper.get().fromState === State.EDITING_NEW) {
this.router.navigate([ '../' ], {relativeTo: this.route});
}
});

this.formHelper.modeObservable.pipe(filter(state => state.action === Action.SAVE))
.subscribe(state => {
this.resetMainForm();
this.form.disable();
this.toastr.success('dictionary has been saved', DefaultTitle.SUCCESS, {
timeOut: 3000,
closeButton: false
});
});

this.setupEntriesDragging();
}

// ... some other class methods
}

Although, we’ve reduced only 2 lines of code, but we started gaining readability already. Going further, these attribute assignments could’ve been also moved into separate methods as well.

Next step would be to move all the method executions and subscriptions into ngOnInit lifecycle hook, where these actually belong and provide a little bit leaner subscription closing mechanism (Hopefully you’ve already noticed that not every subscription is being closed there(!))! Interested in this particular topic? You can read more within my other article:

Let us consider we still use older then version 16 of Angular and we will use Subject combined with takeUntil rxjs operator. We will also create methods for each subscription and execute them from ngOnInit . The output would look like this:

@Component({
selector: 'app-dictionary-details',
templateUrl: './dictionary-details.component.html',
styleUrls: [ './dictionary-details.component.scss' ]
})
class TestComponent implements OnInit, OnDestroy {
// ... some other attributes
private destroy$ = new Subject<void>();
private dictionary$ = new BehaviorSubject(new Dictionary());

formHelper: StateManager = new StateManager();

owningUnits$: Observable<Entry[]> = combineLatest([
this.dictionaryReaderService.getByName(QatDictionaryName.OWNING_ORGANIZATIONAL_UNIT)
.pipe(map(dict => dict?.entries || [])),
this.dictionary$
]).pipe(
map(([entries, dictionary]) => {
const units = this.securityContext.organizationalUnitsFor(this.formHelper.isEditingNew ? 'std.dictionary.create' : 'std.dictionary.update');
return entries.filter(e => (e.valid && units.includes(e.code)) || e.code === get(dictionary, 'owner'));
})
);

standardizationDictionaryNames$: Observable<Entry[]> = this.dictionaryReaderService
.getByName(QatDictionaryName.STANDARDIZATION_DICTIONARY)
.pipe(map(dict => dict?.entries || []));

form: FormGroup = this.formBuilder.group({
displayName: [''],
type: [''],
owner: ['']
}, { validators: atLeastOne(Validators.required, ['displayName', 'type', 'owner'])});

displayedColumnsEntries = DictionaryDetailsComponent.VIEW_MODE_DETAIL_ENTRIES_COLUMNS;
displayedColumnsEntities = DictionaryDetailsComponent.VIEW_MODE_DETAIL_ENTITIES_COLUMNS;

canBeCreated: () => boolean;
canBeUpdated: () => boolean;
canBeCreatedOrUpdated: () => boolean;

constructor(
private readonly dictionaryReaderService: QatDictionaryReaderService,
private readonly securityContext: SecurityContext,
private readonly formBuilder: FormBuilder,
private readonly router: Router,
private readonly route: ActivatedRoute,
private readonly toastr: ToastrService
) {}

ngOnInit(): void {
this.setupForm();
this.listenUserChanges();
this.listenAnyEditingModeChanges();
this.listenViewingModeChanges();
this.listenCancelActionChanges()
this.listenSaveActionChanges();
this.setupEntriesDragging();
}

listenUserChanges(): void {
this.securityContext.userChanges
.pipe(
filter(Boolean),
takeUntil(this.destroy$.asObservable())
).subscribe(user => {
this.canBeCreated = ((entitlement: Entitlement) => () =>
entitlement && entitlement.organizationalUnits.includes(this.form.get('owner').value)
)(_.first(this.securityContext.findMatchingEntitlements('std.dictionary.create')));
this.canBeUpdated = ((entitlement: Entitlement) => () =>
entitlement && entitlement.organizationalUnits.includes(this.form.get('owner').value)
)(_.first(this.securityContext.findMatchingEntitlements('std.dictionary.update')));
this.canBeCreatedOrUpdated = ((entitlements: Entitlement[]) => () => entitlements.length > 0
)(this.securityContext.findMatchingEntitlements(['std.dictionary.create', 'std.dictionary.update']));
});
}

listenAnyEditingModeChanges(): void {
this.formHelper.modeObservable
.pipe(
filter(({ isAnyEditing }) => isAnyEditing),
takeUntil(this.destroy$.asObservable())
).subscribe(state => {
if (this.dictionary.entries != null) {
this.displayedColumnsEntries = DictionaryDetailsComponent.EDIT_MODE_DETAIL_ENTRIES_COLUMNS;
} else {
this.displayedColumnsEntities = DictionaryDetailsComponent.EDIT_MODE_DETAIL_ENTITIES_COLUMNS;
}
this.form.enable();
if (state.action === Action.EDIT) {
this.form.get('name').disable();
this.form.get('owner').disable();
if (this.dictionary.entities.length > 0) {
this.disableColumns();
}
}
});
}

listenViewingModeChanges(): void {
this.formHelper.modeObservable.pipe(
filter(({ isViewing }) => isViewing),
takeUntil(this.destroy$.asObservable())
).subscribe(() => {
if (this.dictionary.entries.length > 0) {
this.displayedColumnsEntries = DictionaryDetailsComponent.VIEW_MODE_DETAIL_ENTRIES_COLUMNS;
this.removeLastEmptyEntry();
} else {
this.displayedColumnsEntities = DictionaryDetailsComponent.VIEW_MODE_DETAIL_ENTITIES_COLUMNS;
}
this.form.disable();
});
}

listenCancelActionChanges(): void {
this.formHelper.modeObservable
.pipe(
filter(({ action }) => action === Action.CANCEL),
takeUntil(this.destroy$.asObservable())
).subscribe(() => {
this.resetMainForm();
this.form.disable();
if (this.formHelper.get().fromState === State.EDITING_NEW) {
this.router.navigate([ '../' ], { relativeTo: this.route });
}
});
}

listenSaveActionChanges(): void {
this.formHelper.modeObservable
.pipe(
filter(({ action }) => action === Action.SAVE),
takeUntil(this.destroy$.asObservable())
).subscribe(() => {
this.resetMainForm();
this.form.disable();
this.toastr.success('dictionary has been saved', DefaultTitle.SUCCESS, {
timeOut: 3000,
closeButton: false
});
});
}

ngOnDestroy(): void {
this.destroy$.next();
}

// ... some other class methods
}

Now our component is a little bit longer, but there’s no memory leak anymore. So we’ve gained readability and fixed some underlaying problems as well. As you can probably imagine, changes we’ve made will help us provide some proper unit testing for each method separately.

As you probably noticed, formatting looks weird, and if you do you’re completely right! The project I’ve used as reference haven’t got any eslint and prettier set up. Which I strongly recommend to use. If you’re not familiar with these tools, there’s already an article of mine waiting for you:

Hopefully this piece, helped you understanding how to organise your components structure and how significant difference it makes.

Looking for some more examples? Stay tuned! Still more to come!

--

--

Kamil Konopka

JavaScript/Typescript experience with biggest corporates and scalable software, with focus on reactive / performance programming approach / high code quality.