Intro
Being a web-developer, we make a lot of buttons...a..metric-crap ton. With the wondrous concept of reusable components in JavaScript and it's various frameworks, it's become a lot easier to save time in writing these repetitive tasks. I've decided to build my own Component Library in an Angular 6 project and share it; this is the first part of a series of articles. You could say I was inspired by the Module Monday series!
TL;DR
Go straight to the AngularComponentLibrary Repo
What I want this button to be capable of handling
TS Accessors (get/set) instead of ngOnChanges bc I wanted more fine grain control to each input and deal with each change as it comes instead of all at once every time whether a change happend or not.
Button used for:
- Standard button with a standard initial styling that is re-usable in all my views; emits to parent component that it has been clicked, business logic handled by the parent component.
- Can be disabled with standard styling based on logic handled by the parent.
- Can be set rendered in a list and can be set as selected as part of a list of items.
- Can show a loading animation that is activated and reset by the parent component (i.e., an Http request).
- Can show updated success message if desired after loading animation based on parent component logic.
Steps
Create the component via Angular CLI
Create the button component. In a production application, I would typically place this component in a Shared Module and export it.
$ ng g c button
Decide on the Inputs and Outputs
I'm going to skip the styling in the article as it's pretty straight-forward and also will likely be changed and set up to match the application being used in, so you can see my base styling for it in the code via the repo link. We'll focus on the actual logic instead. This is a really simple component even with all the inputs, so I'll write it as an inline template component so that the HTML is easy to reference when looking at the logic.
We know this button is going to take inputs from the parent component, that's how it is reusable. We also know that this button needs to provide some kind of signal back to the parent component to let the parent know that the button was clicked. So we'll need to import Input
, Output
, and EventEmitter
as the emitter will signal the parent to do something with the button click.
Given the list above, I need the button to be able to have it's name dynamically set, the button type (button or submit), I also want it's disabled status to be dynamically set and have styling for it, and the button should know when to start/stop the loading animation or whether it's been activated as a selection and of course apply the right styles. So here's the initial inline template portion of the button.component.ts file.
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-button',
template: `
<button type="buttonType" class="button" (click)="onClick()"
[disabled]="isDisabled"
[ngClass]="{'loading' : loading, 'disabled': isDisabled, 'active': isActivated}">{{ buttonText }}
</button>
`,
styleUrls: ['./button.component.scss']
})
And now I need to define the expected inputs. You'll notice that I'm using TS accessors (get/set) for loading, disabled, and activated. This is so that the button component can detect input changes after init. Another way to do this is using ngOnChanges
but I wanted more fine grain control to each input and deal with each change as it comes instead of all at once every time whether a change happend or not for that particular input.
I've also set default values to false so that you don't have to include [loading], [isDisabled], [isActivated] bindings if you don't want / need them. Then the bare minimum button component you needed would be:
<app-button [buttonText]="someText" (buttonClick)="onClick()"></app-button>
export class ButtonComponent implements OnInit {
@Input() buttonText: string;
@Input() buttonSubmit = false;
@Input()
set loading(loading: boolean) {
this._loading = loading || false;
}
get loading(): boolean {
return this._loading;
}
@Input()
set isDisabled(isDisabled: boolean) {
this._isDisabled = isDisabled || false;
}
get isDisabled(): boolean {
return this._isDisabled;
}
@Input()
set isActivated(isActivated: boolean) {
this._isActivated = isActivated || false;
}
get isActivated(): boolean {
return this._isActivated;
}
Now for the output, or the emitter which notifies the parent component to do something now that the button was clicked. This follows the one-way binding convention where the parent should decide what to do, centralizes the logic a bit more and is easier to reason about. So we instantiate an EventEmitter and an accompanying function to the onClick listener that just emits the button's name plus a string, which isn't necessary but useful to verify that the emitter is working later. Also on construction, if [buttonText] wasn't defined, I set it to a string to tell me that the button needs a text value.
@Output() buttonClick: EventEmitter<any>;
constructor() {
this.buttonClick = new EventEmitter<any>();
this.buttonType = this.buttonSubmit ? `submit` : `button`;
}
ngOnInit() {
this.buttonText = this.buttonText ? this.buttonText : `No buttonText`;
}
onClick(): any {
if (this.isDisabled) {
return;
} else {
this.buttonClick.emit(this.buttonText + ` clicked`);
}
}
}
That's it! Now we instantiate a basic button with a loader like this:
<app-button [buttonText]="buttonTitle" [loading]="buttonLoading" (buttonClick)="onClick()"></app-button>
And the parent.component.ts would have something like this, where the timeout portion would be an asynch request such as an HTTP POST. (Don't forget that to reset the loading state on success or failure of the request.)
buttonText = `My new button`;
buttonLoading = false;
buttonClicked(event: any): void {
console.log(event);
this.buttonLoading = true;
this.buttonText = `Component loading`;
this.httpRequestSvc.someRequest().subscribe(data => {
if (data) {
// some logic with the data
this.buttonLoading = false;
this.buttonText = `My new button`;
} else {
this.buttonLoading = false;
this.buttonText = `My new button`;
}
});
}
So that's that, check the repo for the entirety of the code and to see additional documentation I wrote for this component. Let me know in the components on how it's working for you, or if you see areas for improvement!
Also feel free to request components. I'll be working on a gallery component next that auto fits images.
Top comments (0)