Skip to content

Angular Custom Builders: Markdown + Angular

Angular Custom Builders: Markdown + Angular

Since Angular version 8, the Builders API has become stable. In this post, we'll explore how to use it to extend, or add, new commands to the Angular CLI.

Let's build an example project. We'll create a builder that will allow us to use markdown that can be transformed into an html template file for our components. We will also add a requirement: remove all the generated files after building the project.

We'll start by cloning a starter project for angular builders:

git clone git@github.com:flakolefluk/angular-builder-starter.git md-builder // rename the folder to a reasonable name for your project
cd md-builder
npm install

Let's take a look at our folder structure.

Folder structure

src/builders.json

{
  "$schema": "@angular-devkit/architect/src/builders-schema.json",
  "builders": {
    "build": {
      "implementation": "./build",
      "schema": "./build/schema.json",
      "description": "Custom Builder"
    }
  }
}

builders.json contains the required information for the builder that contains our package. The builder will contain a name- in this case build- the location of the builder /build/index.ts or build, a description, and the location of the schema. The schema will provide some information about the builder, and information about the parameters that can be passed to the CLI when running the builder. It's important that package.json points to the builders.json location. Also, remember to rename the package to our desired name for the builder. We'll use this name later to link the package.

{
  "name": "@flakolefluk/md-builder",
  "version": "0.0.1",
  "description": "Starter project for Angular CLI's custom builders.",
  "main": "src/index.js",
  "scripts": {
    "build": "tsc"
  },
  "builders": "src/builders.json",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/flakolefluk/angular-builder-starter.git"
  },
  "keywords": ["angular", "cli", "builder"],
  "author": {
    "name": "Ignacio Falk",
    "email": "flakolefluk@gmail.com"
  },
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/flakolefluk/angular-builder-starter/issues"
  },
  "homepage": "https://github.com/flakolefluk/angular-builder-starter/#readme",
  "devDependencies": {
    "@angular-devkit/architect": "^0.803.0",
    "@angular-devkit/core": "^8.3.0",
    "@types/node": "^12.6.9",
    "prettier": "1.18.2",
    "typescript": "^3.5.3"
  }
}

build/schema.json

{
  "$schema": "http://json-schema.org/schema",
  "title": "Custom builder schema",
  "description": "Custom builder description",
  "type": "object",
  "properties": {
    "log": {
      "type": "boolean",
      "description": "If true, log messages",
      "default": true
    }
  },
  "additionalProperties": false
}

In this starter project, there's a boolean log option. This json file can be used with an interface to have the right typings.

build/schema.ts

export interface Schema {
  log: boolean;
}

Finally, the builder implementation. build/index.ts

import {
  BuilderOutput,
  createBuilder,
  BuilderContext
} from "@angular-devkit/architect";
import { JsonObject } from "@angular-devkit/core";
import { Schema } from "./schema";

async function _build(
  options: JsonObject & Schema,
  context: BuilderContext
): Promise<BuilderOutput> {
  if (options.log) {
    context.logger.info("Building...");
  }

  return { success: true };
}

export default createBuilder(_build);

A builder is a handler function with two arguments:

  • options: a JSON object provided by the user
  • context: A BuilderContext object that provides access to the scheduling method scheduleTarget and the logger among other things.

The builder can return either a Promise or an Observable.

Let's modify our project to fit our needs. We will start with a simple builder, and will start improving it step by step.

When we build our project, we do not need to watch for file changes. It's a one-time process. It has a start and an end. Our build chain will look something like this.

  • Convert markdown into html
  • Execute the regular build process
  • Clear all generated html files

Also, we want the custom builder to work along other builders (the default Angular builders, or other custom builders).

I will use a couple of packages for traversing/watching the project directory, and converting the markdown files into html.

npm i --save marked chokidar @types/marked

Let's take a look at our implementation.

import {
  BuilderOutput,
  createBuilder,
  BuilderContext
} from "@angular-devkit/architect";
import { JsonObject } from "@angular-devkit/core";
import { Schema } from "./schema";
import * as chokidar from "chokidar";
import * as marked from "marked";
import * as path from "path";
import * as fs from "fs";

function readFiles(watcher: chokidar.FSWatcher) {
  return new Promise((resolve, reject) => {
    watcher.on("ready", () => resolve(null));
    watcher.on("error", error => reject(error));
  }).then(_ => watcher.getWatched());
}

function clearFiles(filesToDelete: string[]) {
  filesToDelete.forEach(file => {
    try {
      fs.unlinkSync(file);
    } catch (e) {
      // do nothing
    }
    return null;
  });
}

function convertFile(path: string): string {
  const content = fs.readFileSync(path, { encoding: "utf-8" });
  const html = marked(content).replace(/^\t{3}/gm, "");
  const index = path.lastIndexOf(".");
  const htmlFileName = path.substring(0, index) + ".html";
  fs.writeFileSync(htmlFileName, html);
  return htmlFileName;
}

async function _build(
  options: JsonObject & Schema,
  context: BuilderContext
): Promise<BuilderOutput> {
  if (options.log) {
    context.logger.info("Building...");
  }
  const root = context.workspaceRoot;

  // setup marked
  marked.setOptions({ headerIds: false });

  // start "watching" files.
  const watcher = chokidar.watch(path.join(root, "src", "**", "*.md"));

  // get all markdown files
  const filesMap = await readFiles(watcher);

  // stop watching files
  await watcher.close();

  // convert to array of paths
  const paths = Object.keys(filesMap).reduce((arr, key) => {
    filesMap[key].forEach(file => { if(file.toLowerCase().endsWith('.md')) {
  arr.push(path.join(key, file));
}});
    return arr;
  }, [] as string[]);

  // convert files and return html paths
  let pathsToDelete: string[] = [];
  paths.forEach(path => {
    const toDelete = convertFile(path);
    pathsToDelete.push(toDelete);
  });

  // schedule new target
  const target = await context.scheduleTarget({
    target: "build",
    project: context.target !== undefined ? context.target.project : ""
  });

  // return result (Promise) and clear files if it fails or succeeds
  return target.result.finally(() => clearFiles(pathsToDelete));
}

export default createBuilder(_build);

Let's go step by step. We will start by setting up marked. Then, we start watching our project source directory and subdirectories for markdown files. When the ready event emits, we will return all the watched files. Then, we will proceed to convert all the files, and will keep track of the html files paths. Then, we schedule a target. Targets are set on the angular.json file. In this initial example, we will schedule the build target, and will return its result. After this, the target fails or succeds, and the files will be cleared.

Let's build our custom builder, and link it to test it locally:

npm run build
npm link

It's time to create a project, and test our builder!

ng new builders-example
cd builders-example
npm link @flakolefluk/md-builder // the name of the builder package

Now that our project is set up, and our dependencies are installed, we should:

  • remove app.component.html
  • create app.component.md

My markdown file looks like this:

# MD BUILDER

## this is a test

{{title}} works!

Before we run our builder, we must set it in the project's angular.json file.

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "builders-example": {
      "projectType": "application",
      "schematics": {},
      "root": "",
      "sourceRoot": "src",
      "prefix": "app",
      "architect": {
        "md-build": {
          "builder": "@flakolefluk/md-builder:build"
        },
        "build": {
          // ...
        }
      }
    }
  }
}

I created the md-build target. The builder key sets the target: the build builder in the @flakolefluk/md-builder package. Next to it, we have the build target (remember that our builder will schedule it).

To run a target different than the regular ones (build test, e2e, etc), you must call ng run <project>:<target>. In this example, it would be ng run builders-example:md-build.

Let's try it.

CLI bundle success

Our builder runs the way we expect it to run. Converts the markdown files, builds the project, and removes the generated files.

What if we wanted to schedule another target other than build? What if we wanted to run our command simply as ng build?

Let's add some configuration options to our builder.

build/schema.json

{
  "$schema": "http://json-schema.org/schema",
  "title": "Custom builder schema",
  "description": "Custom builder description",
  "type": "object",
  "properties": {
    "log": {
      "type": "boolean",
      "description": "If true, log messages",
      "default": true
    },
    "target": {
      "type": "string",
      "description": "target to be scheduled after converting markdown"
    }
  },
  "required": ["target"],
  "additionalProperties": false
}

build/schema.ts

export interface Schema {
  log: boolean;
  target: string;
}

build.index.ts

// ...
const target = await context.scheduleTarget({
  target: options.target,
  project: context.target !== undefined ? context.target.project : ""
});
// ...

Don't forget to run npm run build before testing again.

If we try to run our app project with the same command, we will get an error. We need to provide the required option target. We will set this in our angular.json file.

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "builders-example": {
      "projectType": "application",
      "schematics": {},
      "root": "",
      "sourceRoot": "src",
      "prefix": "app",
      "architect": {
        "md-build": {
          "builder": "@flakolefluk/md-builder:build",
          "options": {
            "target": "build"
          }
        },
        "build": {}
      }
    }
  }
}

Now we can run our application using the ng run builders-example:md-build command. Let's make one more change to make the builder easier to use.

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "builders-example": {
      "projectType": "application",
      "schematics": {},
      "root": "",
      "sourceRoot": "src",
      "prefix": "app",
      "architect": {
        "build": {
          "builder": "@flakolefluk/md-builder:build",
          "options": {
            "target": "ng-build"
          }
        },
        "ng-build": {}
      }
    }
  }
}

We changed the target names (remember we can pass any target name to our builder) and now we are able to run this process just by calling ng build.

Our build is working as expected. But our current setup will not work if we want to serve our application during development. We could start a different builder to serve our app, but I'll try to modify this one in a way that can handle both cases (watch mode and a single run)

We'll start by changing how we handle the scheduled target. Initially, we were returning the result property. This property returns the next output from a builder, and it works for single run tasks. If we want to track every output of a builder, then we'll use the output property, which will return an Observable of BuilderOutput.

build/index.ts

// ...
async function setup(
  options: JsonObject & Schema,
  context: BuilderContext
): Promise<{ target: BuilderRun; pathsToDelete: string[] }> {
  const root = context.workspaceRoot;
  marked.setOptions({ headerIds: false });

  const watcher = chokidar.watch(path.join(root, "src", "**", "*.md"));

  const filesMap = await readFiles(watcher);

  await watcher.close();
  const paths = Object.keys(filesMap).reduce((arr, key) => {
    filesMap[key].forEach(file => { if(file.toLowerCase().endsWith('.md')) {
  arr.push(path.join(key, file));
}});
    return arr;
  }, [] as string[]);

  let pathsToDelete: string[] = [];

  paths.forEach(path => {
    const toDelete = convertFile(path);
    pathsToDelete.push(toDelete);
  });
  context.logger.info("files converted");

  const target = await context.scheduleTarget({
    target: options.target,
    project: context.target !== undefined ? context.target.project : ""
  });

  return { target, pathsToDelete };
}

function _build(
  options: JsonObject & Schema,
  context: BuilderContext
): Observable<BuilderOutput> {
  if (options.log) {
    context.logger.info("Building...");
  }

  return from(setup(options, context)).pipe(
    mergeMap(({ target, pathsToDelete }) =>
      target.output.pipe(
        finalize(() => {
          clearFiles(pathsToDelete);
        })
      )
    )
  );
}

export default createBuilder(_build);

We refactor the setup part of our _build method into its own method that returns a Promise. Then, we create an Observable stream from that promise, and return a new Observable that will clear the genreated files once it completes.

Let's build our custom builder, and run the build process in our demo-app. Everything should work the same as before. Let's configure our app to do the same when serving it.

angular.json

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "builders-example": {
      "architect": {
        "build": {},
        "ng-build": {},
        "serve": {
          "builder": "@flakolefluk/md-builder:build",
          "options": {
            "target": "ng-serve"
          }
        },
        "ng-serve": {
          "builder": "@angular-devkit/build-angular:dev-server",
          "options": {
            "browserTarget": "builders-example:ng-build"
          },
          "configurations": {
            "production": {
              "browserTarget": "builders-example:ng-build:production"
            }
          }
        }
      }
    }
  }
}

I renamed the serve target to ng-serve, and added it to the custom builder.

ng serve

Our project works as expected. If we modify any file, it will refresh. However, there are two major issues. If we modify a markdown file, it won't regenerate the html file, and when we kill our process (Ctrl+C), the generated files are not removed.

We need to reconsider how to structure our build/serve process. After a first read of the .md files, we must keep watching for changes (added, changed or removed), and schedule our target. To address the issue when the task is killed, we must listen to the SIGNINT event in our process, then proceed to stop watching the markdown files, and remove the generated files. Finally, exit the process without errors.

import {
  BuilderOutput,
  createBuilder,
  BuilderContext,
  BuilderRun
} from "@angular-devkit/architect";
import { JsonObject } from "@angular-devkit/core";
import { Schema } from "./schema";
import * as chokidar from "chokidar";
import * as marked from "marked";
import * as path from "path";
import * as fs from "fs";
import { Observable, from, fromEvent } from "rxjs";
import { finalize, mergeMap, first, tap } from "rxjs/operators";

function clearFiles(filesToDelete: string[]) {
  filesToDelete.forEach(file => {
    try {
      fs.unlinkSync(file);
    } catch (e) {
      // do nothing
    }
    return null;
  });
}

function toHtmlPath(path: string): string {
  const index = path.lastIndexOf(".");
  const htmlFileName = path.substring(0, index) + ".html";
  return htmlFileName;
}

function convertFile(path: string): string {
  const content = fs.readFileSync(path, { encoding: "utf-8" });
  const html = marked(content).replace(/^\t{3}/gm, "");
  const htmlFileName = toHtmlPath(path);
  fs.writeFileSync(htmlFileName, html);
  return htmlFileName;
}

function removeFile(path: string): string {
  const htmlFileName = toHtmlPath(path);
  fs.unlinkSync(htmlFileName);
  return htmlFileName;
}

function _setup(
  options: JsonObject & Schema,
  context: BuilderContext
): Promise<BuilderRun> {
  return context.scheduleTarget({
    target: options.target,
    project: context.target !== undefined ? context.target.project : ""
  });
}

function _build(
  options: JsonObject & Schema,
  context: BuilderContext
): Observable<BuilderOutput> {
  // setup marked
  marked.setOptions({ headerIds: false });

  // setup markdown watcher and keep track of generated files
  const root = context.workspaceRoot;
  const watcher = chokidar.watch(path.join(root, "src", "**", "*.md"));
  let pathsToDelete: string[] = [];

  // add, update or remove html files on events.
  watcher
    .on("add", (path: string) => {
      const htmlFile = convertFile(path);
      if (options.log) {
        context.logger.info(`${htmlFile} added`);
      }
      pathsToDelete.push(htmlFile);
    })
    .on("change", (path: string) => {
      const htmlFile = convertFile(path);
      if (options.log) {
        context.logger.info(`${htmlFile} changed`);
      }
    })
    .on("unlink", (path: string) => {
      const htmlFile = removeFile(path);
      if (options.log) {
        context.logger.info(`${htmlFile} removed`);
      }
      pathsToDelete = pathsToDelete.filter(path => path !== htmlFile);
    });

  // when the task is killed, stop wtahcing files, and remove generated files
  process.on("SIGINT", () => {
    clearFiles(pathsToDelete);
    watcher.close();
    process.exit(0);
  });

  // wait for the watcher to be ready (after all files have been localized), then schedule the next target, and return its output. If the output completes (for example "ng build"), remove files, and stop watching markdown changes
  return fromEvent(watcher, "ready").pipe(
    tap(() => {
      context.logger.info("Markdown ready...");
    }),
    first(),
    mergeMap(_ => from(_setup(options, context))),
    mergeMap(target =>
      target.output.pipe(
        finalize(() => {
          clearFiles(pathsToDelete);
          watcher.close();
        })
      )
    )
  );
}

export default createBuilder(_build);

Finally, we need to set up our angular.json to run any other CLI command using the custom builder.

Demo video

Final words

  • Feel free to contribute to this project. There's a lot of room for improvement. (Language service does not work on markdown files) :(
  • The code for the builder is located in this repository
  • The sample app is located here
  • The Angular custom builder starter project can be found in here

This Dot Labs is a development consultancy that is trusted by top industry companies, including Stripe, Xero, Wikimedia, Docusign, and Twilio. This Dot takes a hands-on approach by providing tailored development strategies to help you approach your most pressing challenges with clarity and confidence. Whether it's bridging the gap between business and technology or modernizing legacy systems, you’ll find a breadth of experience and knowledge you need. Check out how This Dot Labs can empower your tech journey.

You might also like

A Guide to (Typed) Reactive Forms in Angular - Part II (Building Dynamic Superforms) cover image

A Guide to (Typed) Reactive Forms in Angular - Part II (Building Dynamic Superforms)

In the first blog post of the series, we learned about Angular reactive forms and the data structures behind them. When developing real-world applications, however, you often need to leverage dynamic forms, as writing boilerplate for every form and its specific cases can be tedious and time-consuming. In certain situations, it may even be necessary to retrieve information from an API to construct the forms. In this post, we will go over a convenient abstraction we can create to build dynamic and adaptable forms without repeating boilerplate. The trick is to create a "view model" for our data and use a service to transform that data into a reactive form. I was first introduced to this approach by my friend and former teammate Thomas Duft, and I've been using it ever since. The approach outlined in the linked article worked great with untyped forms, but since now we can get our forms strictly typed, we'll want to upgrade it. And here is where it gets a bit tricky. Remember how I mentioned you shouldn't predeclare your form groups earlier? If you want to recursively create a form from a config, you just have to. And it's a dynamic form, so you cannot easily type it. To solve the issue, I devised a trick inspired by a "Super Form" suggested by Bobby Galli. Assuming we will have interfaces defined for our data, using this approach, we can create dynamic type-safe forms. First, we'll create types for our form config: `TypeScript // this will be our ViewModel for configuring a FormGroup export class FormSection | FormField | (FormSection | FormField)[]; } = any > { public title?: string; public fields: T; constructor(section: { title?: string; fields: T; }) { this.title = section.title; this.fields = section.fields; } } // Let's define some editor types we'll be using in the templates later export type FormEditor = | 'textInput' | 'passwordInput' | 'textarea' | 'checkbox' | 'select'; // And this will be a ViewModel for our FormControls export class FormField { public value: T; public editor: FormEditor; public validators: Validators; public label: string; public required: boolean; public options?: T[]; constructor(field: { value: T; editor: FormEditor; validators: Validators; label: string; required: boolean; options?: T[]; }) { this.value = field.value; this.editor = field.editor; this.validators = field.validators; this.label = field.label; this.required = field.required; this.options = field.options; } } ` And then we'll create some type mappings: `TypeScript // We will use this type mapping to properly declare our form group export type ControlsOf> = { [K in keyof T]: T[K] extends Array ? FormArray> : T[K] extends Record ? FormGroup> : FormControl; }; // We will use this type mapping to type our form config export type ConfigOf = { [K in keyof T]: T[K] extends (infer U)[] ? U extends Record ? FormSection>[] : FormField[] : T[K] extends Record ? FormSection> : FormField; }; ` And now we can use our types in a service that will take care of creating nested dynamic forms: `TypeScript import { Injectable } from '@angular/core'; import { AbstractControl, FormArray, FormControl, FormGroup, } from '@angular/forms'; import { ConfigOf, ControlsOf, FormField, FormSection } from './forms.model'; @Injectable({ providedIn: 'root', }) export class FormsService { public createFormGroup>( section: FormSection> ): FormGroup> { // we need to create an empty FormGroup first, so we can add FormControls recursively const group = new FormGroup({}); Object.keys(section.fields).forEach((key: any) => { const field = section.fields[key]; if (Array.isArray(field)) { group.addControl(key, this.createFormArray(field)); } else { if (field instanceof FormSection) { group.addControl(key, this.createFormGroup(field)); } else { group.addControl(key, new FormControl(field.value, field.validators)); } } }); // and we need to cast the group to the correct type before returning return group as unknown as FormGroup>; } public createFormArray>( fields: unknown[] ): FormArray> { const array: FormArray> = new FormArray( [] ) as unknown as FormArray>; fields.forEach((field) => { if (field instanceof FormSection) { array.push(this.createFormGroup(field)); } else { const { value, validators } = field as FormField; array.push(new FormControl(value, validators)); } }); return array as unknown as FormArray>; } } ` And that's it. Now we can use our FormService` to create forms. Let's say we have the following User model: `TypeScript export type User = { email: string; name: string; } ` We can create a form for this user from config in the following way: `TypeScript const userFormConfig = new FormSection>({ title: 'User Form', fields: { email: new FormField({ value: '', validators: [Validators.required, Validators.email], label: 'Email', editor: 'textInput', required: true, }), name: new FormField({ value: '', validators: [Validators.required], label: 'Name', editor: 'textInput', required: true, }) } }); const userForm = this.formsService.createFormGroup(userFormConfig); ` If we would check the type of userForm.value` now, we would see that it's correctly inferred as: `TypeScript Partial ` Outputting the Dynamic Forms To display the dynamic forms, we can write a simple component that takes the FormSection` or `FormField` as an `Input()` along with our `FormGroup` and displays the form recursively. We can use a setter to assign either field` or `section` property when the view model is passed into the component, so we can conveniently use them in our template. Our form component's TypeScript code will look something like this: `TypeScript import { Component, Input } from '@angular/core'; import { FormField, FormSection } from '../forms.model'; import { FormArray, FormGroup } from '@angular/forms'; @Component({ selector: 'app-form', templateUrl: './form.component.html', styleUrls: ['./form.component.scss'], }) export class FormComponent { private fieldConfig?: FormField; private sectionConfig?: FormSection; private arrayConfig?: (FormSection | FormField)[]; private sectionFieldsArray?: [string, FormField][]; @Input() public set config( config: | FormField | FormSection | (FormSection | FormField)[] ) { this.fieldConfig = config instanceof FormField ? config : undefined; this.arrayConfig = Array.isArray(config) ? config : undefined; this.sectionConfig = config instanceof FormSection ? config : undefined; this.sectionFieldsArray = Object.entries(this.sectionConfig?.fields || {}); } public get sectionFields(): [string, FormField][] { return this.sectionFieldsArray || []; } public get field(): FormField | undefined { return this.fieldConfig; } public get section(): FormSection | undefined { return this.sectionConfig; } public get array(): (FormSection | FormField)[] | undefined { return this.arrayConfig; } ngAfterViewInit() { console.log(this.arrayConfig); } @Input() public key!: string; @Input() public group!: FormGroup; public get sectionGroup(): FormGroup { return this.group.get(this.key) as FormGroup; } public get formArray(): FormArray { return this.group.get(this.key) as FormArray; } } ` And our template will reference a new form component for each section field in case we have passed in a FormSection` and it will have a switch case to display the correct control in case a `FormField` has been passed in: `HTML {{ field.label }} {{ section.title }} ` That way, we can display the whole form just by referencing one component, such as `HTML ` Check out an example on StackBlitz. In the next (and last) post of the series, we will learn about building custom Form Controls....

A Guide to (Typed) Reactive Forms in Angular - Part I (The Basics) cover image

A Guide to (Typed) Reactive Forms in Angular - Part I (The Basics)

When building a simple form with Angular, such as a login form, you might choose a template-driven approach, which is defined through directives in the template and requires minimal boilerplate. A barebone login form using a template-driven approach could look like the following: `HTML E-mail Password Login! ` `TypeScript // login.component.ts import { Component } from '@angular/core'; @Component({ selector: 'app-login', templateUrl: './login.component.html' }) export class LoginComponent implements OnInit { public credentials = { email: '', password: '' }; constructor(public myAuthenticationService: MyAuthenticationService) { } } ` However, when working on a user input-heavy application requiring complex validation, dynamic fields, or a variety of different forms, the template-driven approach may prove insufficient. This is where reactive forms** come into play. Reactive forms employ a reactive approach, in which the form is defined using a set of form controls and form groups. Form data and validation logic are managed in the component class, which updates the view as the user interacts with the form fields. This approach requires more boilerplate but offers greater explicitness and flexibility. In this three-part blog series, we will dive into the reactive forms data structures, learn how to build dynamic super forms, and how to create custom form controls. In this first post, we will familiarize ourselves with the three data structures from the @angular/forms` module: FormControl The FormControl` class in Angular represents a single form control element, such as an input, select, or textarea. It is used to track the value, validation status, and user interactions of a single form control. To create an instance of a form control, the `FormControl` class has a constructor that takes an optional initial value, which sets the starting value of the form control. Additionally, the class has various methods and properties that allow you to interact with and control the form control, such as setting its value, disabling it, or subscribing to value changes. As of Angular version 14, the FormControl` class has been updated to include support for **typed reactive forms** - a feature the Angular community has been wanting for a while. This means that it is now a generic class that allows you to specify the type of value that the form control will work with using the type parameter ``. By default, `TValue` is set to any, so if you don't specify a type, the form control will function as an untyped control. If you have ever updated your Angular project with ng cli` to version 14 or above, you could have also seen an `UntypedFormControl` class. The reason for having a `UntypedFormControl` class is to support incremental migration to typed forms. It also allows you to enable types for your form controls after automatic migration. Here is an example of how you may initialize a FormControl` in your component. `TypeScript import { FormControl } from '@angular/forms'; const nameControl = new FormControl("John Doe"); ` Our form control, in this case, will work with string` values and have a default value of "John Doe". If you want to see the full implementation of the FormControl` class, you can check out the Angular docs! FormGroup A FormGroup` is a class used to group several `FormControl` instances together. It allows you to create complex forms by organizing multiple form controls into a single object. The `FormGroup` class also provides a way to track the overall validation status of the group of form controls, as well as the value of the group as a whole. A FormGroup` instance can be created by passing in a collection of `FormControl` instances as the group's controls. The group's controls can be accessed by their names, just like the controls in the group. As an example, we can rewrite the login form presented earlier to use reactive forms and group our two form controls together in a FormGroup` instance: `TypeScript // login.component.ts import { FormControl, FormGroup, Validators } from '@angular/forms'; @Component({ selector: 'app-login', templateUrl: './login.component.html' }) export class LoginComponent implements OnInit { public form = new FormGroup({ email: new FormControl('', [Validators.required, Validators.email]), password: new FormControl('', [Validators.required]), }); constructor(public myAuthenticationService: MyAuthenticationService) { } public login() { // if you hover over "email" and "password" in your IDE, you should see their type is inferred console.log({ email: this.form.value.email, password: this.form.value.password }); this.myAuthenticationService.login(this.form.value); } } ` `HTML E-mail Password Login! ` Notice we have to specify a formGroup` and a `formControlName` to map the markup to our reactive form. You could also use a `formControl` directive instead of `formControlName`, and provide the `FormControl` instance directly. FormArray As the name suggests, similar to FormGroup`, a `FormArray` is a class used to group form controls, but is used to group them in a collection rather than a group. In most cases, you will default to using a FormGroup` but a `FormArray` may come in handy when you find yourself in a highly dynamic situation where you don't know the number of form controls and their names up front. One use case where it makes sense to resort to using FormArray` is when you allow users to add to a list and define some values inside of that list. Let's take a TODO app as an example: `TypeScript import { Component } from '@angular/core'; import { FormArray, FormControl, FormGroup } from '@angular/forms'; @Component({ selector: 'app-todo-list', template: Add TODO , }) export class TodoListComponent { public todos = new FormArray>([]); public todoForm = new FormGroup({ todos: this.todos, }); addTodo() { this.todoForm.controls['todos'].push(new FormControl('')); } } ` In both of the examples provided, we instantiate FormGroup directly. However, some developers prefer to pre-declare the form group and assign it within the ngOnInit method. This is usually done as follows: `TypeScript // login.component.ts import { FormControl, FormGroup, Validators } from '@angular/forms'; @Component({ selector: 'app-login', templateUrl: './login.component.html' }) export class LoginComponent implements OnInit { // predeclare the form group public form: FormGroup; constructor(public myAuthenticationService: MyAuthenticationService) { } ngOnInit() { // assign in ngOnInit this.form = new FormGroup({ email: new FormControl('', [Validators.required, Validators.email]), password: new FormControl('', [Validators.required]), }); } public login() { // no type inference :( console.log(this.form.value.email); } } ` If you try the above example in your IDE, you'll notice that the type of this.form.value` is no longer inferred, and you won't get autocompletion for methods such as `patchValue`. This is because the FormGroup type defaults to `FormGroup`. To get the right types, you can either assign the form group directly or explicitly declare the generics like so: `TypeScript public form: FormGroup, password: FormControl, }>; ` However, explicitly typing all your forms like this can be inconvenient and I would advise you to avoid pre-declaring your FormGroups` if you can help it. In the next blog post, we will learn a way to construct dynamic super forms with minimal boilerplate....

Building Mobile Applications with Svelte and NativeScript cover image

Building Mobile Applications with Svelte and NativeScript

Have you ever wanted to build a mobile application using a language you already know? In this tutorial, we'll learn how to start building a mobile application using Svelte and NativeScript. What is NativeScript NativeScript is a framework that will allow you to write Native apps using JavaScript or Typescript and, at the same time, it allows you to access platform-specific Native APIs. Setting up your environment The very first step to developing with NativeScript is installing all the required dependencies. If you're lucky, you'll already have everything installed. But if not, we'll see how to get it to work. The first thing (assuming you already have Node installed) is to install NativeScript globally. `bash npm i -g nativescript ` For this tutorial, I'll be developing an iOS application. The best way to check if your environment is prepared is to use the command provided by NativeScript. `bash ns doctor ios ` There's an equivalent command for android if that's your target OS. If you are missing anything, you'll get a bunch of messages with the requirements needed. In my case, I had to install XCode, Ruby, Some Gems, and Python libraries. Please refer to the setup guide to check what you need (macOS + iOS, for me). It's important to have your environment ready. Otherwise, we will not be able to run our project. `bash ✔ Getting environment information No issues were detected. ✔ Xcode is installed and is configured properly. ✔ xcodeproj is installed and is configured properly. ✔ CocoaPods are installed. ✔ CocoaPods update is not required. ✔ CocoaPods are configured properly. ✔ Your current CocoaPods version is newer than 1.0.0. ✔ Python installed and configured correctly. ✔ The Python 'six' package is found. ✔ Xcode version 13.4.1 satisfies minimum required version 10. ✔ Getting NativeScript components versions information... ✔ Component nativescript has 8.3.2 version and is up to date. ✔ Component @nativescript/core has 8.3.2 version and is up to date. ✔ Component @nativescript/ios has 8.3.2 version and is up to date. ` Starting a new project Now that our environment is set up, let's start a new project. We are going to base this example app on this sample from the NativeScript samples page. However, we will add more to it, like including HTTP requests, and a List/Detail navigation. `bash ns create sveltapp --svelte ✔ Do you want to help us improve NativeScript by automatically sending anonymous usage statistics? We will not use this information to identify or contact you. … no ` The command will create a Svelte + NativeScript project, and install the required dependencies. Our folder structure will look like this: We'll be focusing on the /app` folder where our JS/TS will be added. `html let message: string = "Blank Svelte Native App" .info .fas { color: #3A53FF; } .info { font-size: 20; horizontal-align: center; vertical-align: center; } ` `html import Home from './components/Home.svelte' ` App.svelte` will render the default page `Home` which contains a message and an icon. You'll notice that these are not the HTML tags you're used to, and that's because these are not HTML elements. These are native elements/views. So if you were thinking of reusing your code in a web app, for example, this is not the part that you'll be able to share. If that's what you need, make sure you extract the pieces that can be reused. Building Our Application Let's delete the content from Home.svelte`, and let's talk about what we'll be building. The app we will build will display a list of items, and when clicked, will navigate into a detailed view. The date for this example will be fetched from the PokeAPI. Creating a List view Let´s create our Home page a.k.a the list. But first, let's create some types to be used in our list. `ts export type PokemonListApiResponse = { count: number, next: string | null, previous: string | null results: Array } export type PokemonListItem = { name: string, image: string } ` First, we created a type for the rest API response. It will contain a list of Pokémon with a few properties. We also create a type for the model used in our view. `html import { Template } from "svelte-native/components"; import { PokemonListItem } from "/types/pokemon"; let data: PokemonListItem[] = []; ` Our page consists of a set of layouts and a list view. Each list item will display an image and a label. However, our view is missing data. Integrating with PokeAPI Let's create a service that will fetch a list of Pokémon. `ts // services/api.ts import { Http } from "@nativescript/core"; import { PokemonListApiResponse, PokemonListItem } from "/types/pokemon"; export const catchemAll = (limit = 100, offest = 0) => Http.getJSON( https://pokeapi.co/api/v2/pokemon?limit=100&offset=0` ).then( (res) => { return res.results.map((pokemon) => { let splitUrl = pokemon.url.split("/"); let id = splitUrl[splitUrl.length - 2]; return { ...pokemon, image: https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${id}.png`, } as PokemonListItem; }); } ); ` To make a request I'll use the Http` API provided by NativeScript. It contains a set of methods that I recommend you to check out before deciding which one will suit better for your use case. I'm making a little transformation to the received data, to build the URL with the sprite of each item. Once our service is in place, we'll fetch data and display it. By default, we will fetch 100 items starting from the first one. I'll make a request once the item is mounted, which means I'll first render the action bar only, and when the response is received, the list will be rendered. `html import { Template } from "svelte-native/components"; import { onMount } from "svelte"; import as api from "../services/api"; import { PokemonListItem } from "/types/pokemon"; let data: PokemonListItem[] = []; onMount(() => { api.catchemAll().then((items) => (data = items)); }); ` Our app should look this by now Much better! Adding the Details view For our sample application to be complete, we want to be able to navigate to a more detailed page of a Pokémon when one list item is selected. First, we need to create our destination component: a detailed view of a Pokémon. We will be adding fetching a description for the Pokémon, but there's a lot of information you can get from the API. Let's add this call to the API service, and create our detailed view. `ts // services/api.ts // ... other methods export const getDescription = (id: number) => Http.getJSON(https://pokeapi.co/api/v2/characteristic/${id}`).then( (result: any) => { let desc = result.descriptions.find( (description: any) => description?.language?.name == "en" ); return desc?.description }, ); ` `html import { onMount } from "svelte"; import as api from "../services/api"; export let index: number; export let item: any; let description = ""; onMount(() => { api.getDescription(index + 1).then( (res) => { description = res || "No description"; }, (e) => { description = "Error fetching data"; } ); }); ` We will be using a set of stackLayout` to build our view, display the image, add the name to the action bar, and finally show a loading message while getting the description, and an error if it fails. Notice there are two properties exported. This means that these properties must be passed as inputs from a parent. Bringing all together It's time to connect the list view, and the details view. Let's see the final Home Page: `html import { navigate } from "svelte-native"; import { Template } from "svelte-native/components"; import { ItemEventData } from "@nativescript/core"; import { onMount } from "svelte"; import Details from "./Details.svelte"; import as api from "../services/api"; import { PokemonListItem } from "/types/pokemon"; let data: PokemonListItem[] = []; onMount(() => { api.catchemAll().then((items) => (data = items)); }); function handleTap(event: ItemEventData) { navigate({ page: Details, props: { index: event.index, item: data[event.index] }, }); } ` We have now included an event listener (on:itemTap`), that will then call the navigate method included in `svelte-native`. Here, we passed the required props `item` and `index` to the Details Component. Now we have connected both views. We don't have to think about a back button in our details view because it's automatically there when you navigate, and add a view to the navigation stack. The final result: Success. Publishing your app If you want to publish the application start by running ns prepare ios --release` Open the project in XCode and follow the instructions on how to publish an iOS app. Final words Being able to work on a native application using a language and a framework you're comfortable with can be a benefit. But there's a cost to it. Setting up the environment is not as straightforward as it could be, but thankfully, the CLI makes it a lot easier to diagnose what's required. There's a lot more to explore in NativeScript like using plugins, but it's out of scope for this post, where we explore the usage of Svelte with NativeScript. You can check the code from this tutorial in this repo....

Being a CTO at Any Level: A Discussion with Kathy Keating, Co-Founder of CTO Levels cover image

Being a CTO at Any Level: A Discussion with Kathy Keating, Co-Founder of CTO Levels

In this episode of the engineering leadership series, Kathy Keating, co-founder of CTO Levels and CTO Advisor, shares her insights on the role of a CTO and the challenges they face. She begins by discussing her own journey as a technologist and her experience in technology leadership roles, including founding companies and having a recent exit. According to Kathy, the primary responsibility of a CTO is to deliver the technology that aligns with the company's business needs. However, she highlights a concerning statistic that 50% of CTOs have a tenure of less than two years, often due to a lack of understanding and mismatched expectations. She emphasizes the importance of building trust quickly in order to succeed in this role. One of the main challenges CTOs face is transitioning from being a technologist to a leader. Kathy stresses the significance of developing effective communication habits to bridge this gap. She suggests that CTOs create a playbook of best practices to enhance their communication skills and join communities of other CTOs to learn from their experiences. Matching the right CTO to the stage of a company is another crucial aspect discussed in the episode. Kathy explains that different stages of a company require different types of CTOs, and it is essential to find the right fit. To navigate these challenges, Kathy advises CTOs to build a support system of advisors and coaches who can provide guidance and help them overcome obstacles. Additionally, she encourages CTOs to be aware of their own preferences and strengths, as self-awareness can greatly contribute to their success. In conclusion, this podcast episode sheds light on the technical aspects of being a CTO and the challenges they face. Kathy Keating's insights provide valuable guidance for CTOs to build trust, develop effective communication habits, match their skills to the company's stage, and create a support system for their professional growth. By understanding these key technical aspects, CTOs can enhance their leadership skills and contribute to the success of their organizations....