#12 Adding a Checkbox Example
Friday, September 28, 2018
By the time we are done with this article we will have created a base class, a pug mixin, and a scss mixin that will make it very easy for us to add example components for each of our controls. We will also have used these tools to create an additional example component for our checkbox control.
Parts
- Part 14: Pipes to the Rescue
- Part 13: Rest of the Examples
- Part 12: Checkbox Example
- Part 11: Adding a Radio Button
- Part 10: Adding a Radio Group
- Part 9: Adding a Select
- Part 8: Adding a Checkbox
- Part 7: Adding a Textarea
- Part 6: Highlighting with Prismjs
- Part 5: Form Snippets Manager
- Part 4: Accessing Form Data
- Part 3: Angular Form Group Interop
- Part 2: The Form Group
- Part 1: Forms Project Creation
An eye to improving change detection in the future
-
WebUi
-
Source
- package.json
-
Source
We are going to start by adding a package to our project. The main reason we are doing this is to have access to immutable data structures that we can use to improve how change detection is handled in our project. Another readon is just to learn more about a popular javascript package.
npm install (1)
npm install --save-dev immutable
package.json (1)
{
...,
"devDependencies": {
...
"immutable": "^3.8.2",
...
}
}
Updating our snippets manager
-
WebUi
-
Source
-
forms
- snippets-manager.ts
-
forms
-
Source
The first order of business is to make some changes to our snippets manager. As it stands now we are creating
the snippets manager within our basic example component. Since we will be refactoring as much as we can into
a base class we need to provide a way to add snippets to the manager after it has been created. To do this
we will convert the internal snippets from a readonly only javascript map to an immutable map and provide
a method for adding snippets (c). With this done we need to adjust
how the internal map is converted to an observable. While we are at it we are going to update the
snippetsToCode
method processes our strings by only allowing
distinct items to continue through the process.
snippets-manager.ts (1)
import { Map } from "immutable";
import { ..., pairs, ... } from "rxjs";
import { distinct, ... } from "rxjs/operators";
export type Snippet = { component?: string; template: string; };
export type SnippetArray = Array<[string, Snippet]>;
export class SnippetsManager {
private _snippets: Map<string, Snippet> = null; <-- Remove readonly
constructor(snippets: Array<[string, Snippet]>) { <-- Remove constructor
this._snippets = new Map(snippets);
}
public addSnippets = (snippets: SnippetArray) => {
this._snippets = new Map(snippets);
}
public getComponent = (key: string) => {
const snippet = this._snippets.get(key);
if(typeof snippet === "undefined" || snippet === null) {
return `missing snippet for ${key}`;
}
return snippet.component || "// none required";
}
public getTemplate = (key: string) => {
const snippet = this._snippets.get(key);
if(typeof snippet === "undefined" || snippet === null) {
return `missing snippet for ${key}`;
}
return snippet.template || `missing template for ${key}`;
}
...
private snippetsToCode = (indent: number, f: (s: Snippet) => string) => {
let res: string;
pairs(this._snippets.toObject())
.pipe(
map((x: [string, Snippet]) => this.indent(indent, f(x[1]))),
distinct(x => x),
filter( ... ),
...
)
.subscribe( ... );
return res;
}
...
}
The base class that all example component will inherit from
-
WebUi
-
Source
-
forms
-
examples
- examples.component.ts
- examples.mixins.scss
- examples.mixins.pug
-
examples
-
forms
-
Source
Now it is time to create the class and corresponding mixins that we need in order to make creating additional example components as easy as possible. We will start by creating a pug mixin for the templates (d). With this mixin all we have to do is provide the controls that are relevant to a particular example. Next up is the styling. This is even easier since all we have to do is create a mixin and copy over all of the code from the basic example's style file (e). Last up is creating the base typescript class (f).
examples.mixins.pug
mixin example
div.example-container
div.example-content
div.form-data
div.form-data-heading
h2 Form
div.validity.valid(*ngIf="tValid" ) Valid
div.validity.invalid(*ngIf="tInvalid" ) Invalid
h3.form-output-heading Data
div.form-data-output {{tData}}
hr
h3.form-output-heading Errors
div.form-data-output {{tErrors}}
div.controls-template-component
xc-form-group(#form="", name="Form" )
block
hr
div.template-component
div.template
label Template
pre
code.language-pug {{tTemplate}}
div.component
label Component
pre
code.language-typescript {{tComponent}}
...
examples.mixins.scss (1)
@mixin example {
.example-container {
display: flex;
height: 100%;
}
.example-content {
display: grid;
grid-template-columns: auto 1fr;
grid-template-areas: "form-data controls";
flex: 1;
}
$gutter: 0.25em;
$form-data-width: 13em;
$form-data-background-color: white;
$form-data-border-color: gray;
.form-data {
grid-area: form-data;
width: $form-data-width + 2*$gutter;
border-right: 1px solid $form-data-border-color;
background-color: $form-data-background-color;
padding: $gutter;
.form-data-heading {
border-bottom: 1px solid $form-data-border-color;
display: flex;
flex-direction: row;
align-items: center;
padding-bottom: 0.25em;
margin-bottom: 1.5em;
h2 {
flex: 1;
margin: 0;
}
.validity {
padding: 0.25em;
color: white;
width: 3.5em;
text-align: center;
&.valid {
background-color: #34a934;
}
&.invalid {
background-color: #9a2a2a;
}
}
}
.form-output-heading {
margin: 0;
font-size: 1.15em;
}
.form-data-output {
background-color: $form-data-background-color;
border: none;
box-shadow: none;
overflow: auto;
white-space: pre;
margin-bottom: 0.25em;
}
}
.controls-template-component {
grid-area: controls;
padding: $gutter;
overflow: auto;
}
.control-row {
display: grid;
grid-template-columns: auto 1fr 1fr;
grid-template-areas: "control template component";
grid-column-gap: $gutter;
.control {
grid-area: control;
width: 18.75em;
}
.component {
grid-area: component;
}
.template {
grid-area: template;
}
pre {
margin-top: 0;
}
}
.template-component {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-areas: "template component";
grid-column-gap: $gutter;
.component {
grid-area: component;
}
.template {
grid-area: template;
}
}
}
examples.component.ts (1)
import { AfterViewInit, ViewChild } from "@angular/core";
import { SnippetArray, SnippetsManager, SyntaxHighlightingService, XcFormGroupComponent } from "../forms.index";
export abstract class ExamplesComponent implements AfterViewInit {
@ViewChild("form") private readonly _form: XcFormGroupComponent = null;
private get tData() { return JSON.stringify(this._form.data, null, 2); }
private get tErrors() { return JSON.stringify(this._form.errors, null, 2); }
private get tInvalid() { return this._form.invalid; }
private get tValid() { return this._form.valid; }
protected readonly snippetsManager: SnippetsManager = new SnippetsManager();
protected abstract get snippets(): SnippetArray;
protected abstract get tComponent(): string;
protected abstract get tTemplate(): string;
constructor(private readonly _syntaxHighlighter: SyntaxHighlightingService) { }
public ngAfterViewInit(): void {
this._syntaxHighlighter.highlightAll();
}
protected addSnippets = (snippets: SnippetArray) => {
this.snippetsManager = new SnippetsManager(snippets);
}
private tGetSnippetComponent = (key: string) => {
return this.snippetsManager.getComponent(key);
}
private tGetSnippetTemplate = (key: string) => {
return this.snippetsManager.getTemplate(key);
}
}
Updating our basic example
-
WebUi
-
Source
-
forms
-
examples
-
basic
- basic-example.component.pug
- basic-example.component.scss
- basic-example.component.ts
-
basic
-
examples
-
forms
-
Source
Now it's time to put to work everything that we did in the previous section. We will start by updating the template by using the pug mixin (g). Next up, and easiest of all, is the scss changes (h). Both of these are the entire contents of their respective files. Now it's time to deal with the component definition (i).
basic-example.component.pug (1)
include ../examples.mixins.pug
+example
+controlRow("checkbox")
xc-checkbox(label="Checkbox", name="checkbox")
+controlRow("input")
xc-input(label="Input", name="input" )
+controlRow("radio")
xc-radio-group(label="Radio", name="radio", [options]="tOptions")
+controlRow("select")
xc-select(label="Select", name="select", [options]="tOptions")
+controlRow("textarea")
xc-textarea(label="Textarea", name="textarea" )
basic-example.component.scss (1)
@import "../examples.mixins.scss";
@include example;
basic-example.component.ts (1)
import { Component, Inject } from "@angular/core";
import { ExamplesComponent } from "../examples.component";
import { SnippetArray, SyntaxHighlightingService } from "../../forms.index";
@Component({
selector: "basic-example",
template: require("./basic-example.component.pug"),
styles: [require("./basic-example.component.scss")]
})
export class BasicExampleComponent extends ExamplesComponent {
protected get snippets(): SnippetArray {
return [
["checkbox", { template: 'xc-checkbox(label="Checkbox", name="checkbox")' }],
["input", { template: 'xc-input(label="Input", name="input" )' }],
["radio", {
component: `private get tOptions() {
return [
{ label: "label1", value: "value1" },
{ label: "label2", value: "value2" },
{ label: "label3", value: "value3" },
{ label: "label4", value: "value4" }
];
}`,
template: 'xc-radio-group(label="Radio", name="radio", [options]="tOptions" )'
}],
["select", {
component: `private get tOptions() {
return [
{ label: "label1", value: "value1" },
{ label: "label2", value: "value2" },
{ label: "label3", value: "value3" },
{ label: "label4", value: "value4" }
];
}`,
template: 'xc-select(label="Select", name="select", [options]="tOptions")'
}],
["textarea", { template: 'xc-textarea(label="Textarea", name="textarea" )' }]
];
}
protected get tComponent() {
return `import { Component } from "@angular/core";
import { XcFormGroupComponent } from "../../forms.index";
@Component({
selector: "basic-example",
template: require("./basic-example.component.pug"),
styles: [require("./basic-example.component.scss")]
})
export class BasicExampleComponent {
${this.snippetsManager.snippetsToComponent()}
}`;
}
private get tOptions() {
return [
{ label: "label1", value: "value1" },
{ label: "label2", value: "value2" },
{ label: "label3", value: "value3" },
{ label: "label4", value: "value4" }
];
}
protected get tTemplate() {
return `xc-form-group(name="Form" )
${this.snippetsManager.snippetsToTemplate(1)}`;
}
constructor(@Inject(SyntaxHighlightingService) syntaxHighlighter: SyntaxHighlightingService) {
super(syntaxHighlighter);
this.addSnippets(this.snippets);
}
}
How easy is it to make a new example component?
-
WebUi
-
Source
-
forms
-
examples
-
checkbox
- checkbox-example.component.pug
- checkbox-example.component.scss
- checkbox-example.component.ts
-
checkbox
-
examples
-
forms
-
Source
With the refactoring of our basic example done we now turn to creating a new example component for our checkbox control. It is now a pretty easier affair to create it's template (j), styling (k), and component definition (l).
checkbox-example.component.pug (1)
include ../examples.mixins.pug
+example
+controlRow("checkbox")
xc-checkbox(label="Checkbox", name="checkbox")
checkbox-example.component.scss (1)
@import "../examples.mixins.scss";
@include example;
checkbox-example.component.ts (1)
import { Component, Inject } from "@angular/core";
import { ExampleComponent } from "../example.component";
import { SnippetArray, SyntaxHighlightingService } from "../../forms.index";
@Component({
selector: "checkbox-example",
template: require("./checkbox-example.component.pug"),
styles: [require("./checkbox-example.component.scss")]
})
export class CheckboxExampleComponent extends ExampleComponent {
protected get snippets(): SnippetArray {
return [
["checkbox", { template: 'xc-checkbox(label="Checkbox", name="checkbox")' }]
];
}
protected get tComponent() {
return `import { Component } from "@angular/core";
import { XcFormGroupComponent } from "../../forms.index";
@Component({
selector: "checkbox-example",
template: require("./checkbox-example.component.pug"),
styles: [require("./checkbox-example.component.scss")]
})
export class CheckboxExampleComponent {
${this.snippetsManager.snippetsToComponent()}
}`;
}
protected get tTemplate() {
return `xc-form-group.form-group(name="Form" )
${this.snippetsManager.snippetsToTemplate(1)}`;
}
constructor(@Inject(SyntaxHighlightingService) syntaxHighlighter: SyntaxHighlightingService) {
super(syntaxHighlighter);
this.addSnippets(this.snippets);
}
}
Can't use it till it's registered
-
WebUi
-
Source
-
forms
-
examples
- form-examples.component.pug
- form-examples.index.ts
- form-examples.module.ts
-
examples
-
forms
-
Source
With the checkbox example created we need to register it with our module. First up, of course, is to export it from our examples index file (m). Next we will update our links to contain an anchor tag that will point to our new example component (n). And last but not least the actual creating of the route and registering with the module (o).
form-examples.index.ts (1)
...
export * from "./checkbox/checkbox-example.component";
...
form-examples.component.pug (1)
nav.main-nav
ul
li
a(routerLink="/basic", routerLinkActive="active-link") Basic
li
a(routerLink="/checkbox", routerLinkActive="active-link") Checkbox
div.router-outlet
router-outlet
form-examples.module.ts (1)
...
import {
..., CheckboxExampleComponent, ...
} from "./form-examples.index";
const routes: Routes = [
...,
{
path: "checkbox",
component: CheckboxExampleComponent
}
];
@NgModule({
...,
declarations: [
...,
CheckboxExampleComponent,
...
],
...
})
export class FormExamplesModule { }
platformBrowserDynamic().bootstrapModule(FormExamplesModule);
Now that we have an easy way to create example components I will add an example for the rest of our controls and we will pick up from there in the next article.