Showing posts with label Angular. Show all posts
Showing posts with label Angular. Show all posts

Tuesday, June 9, 2026

Angular Constructor vs. ngOnInit

When I learned Angular (coming from an angularJS background) I learned to always use the lifecycle hooks provided by the library and to use the constructor of the component only for dependency injection. I don't remember if that second bit was a formal recommendation or just something I picked up along the way, but it's a "rule" I've stuck with for years now. Lately, I've been seeing more and more logic going into component constructors and it's been bothering me. A lot. So I finally decided I needed a solid understanding of what each section is for and, of course, when I learn something new I document it here so I can find it more easily later.

I know the name of the blog is "Answers I Couldn't Find Anywhere Else", but that hasn't really been the case in a long time and I did find an answer I really liked to this question so I'm going to copy it here. This is from a Medium article by "Matthew" in December 2024.

1. constructor()

  • Purpose: The constructor() is a TypeScript feature and is used for initializing class members and dependency injection. It is called when an instance of the component class is created, before any Angular-specific processing begins.
  • When it’s called: The constructor() is called first when the component instance is created. This happens before Angular initializes the component's inputs or runs any lifecycle hooks like ngOnInit().

Usage:

  • Injecting services or dependencies into the component.
  • Initializing class properties or variables (though it is not recommended to do any complex logic in the constructor).

Example:

2. ngOnInit()

  • Purpose: The ngOnInit() is a lifecycle hook provided by Angular, specifically for initialization logic after Angular has set all the component's inputs and has finished setting up the component. It is typically used to perform component initialization tasks such as fetching data from APIs or executing logic that depends on the input properties being set.
  • When it’s called: ngOnInit() is called after the component is created, Angular has fully initialized the inputs, and the first change detection pass has been completed. This means that by the time ngOnInit() is called, Angular has set the @Input properties and any other bindings.

Usage:

  • Fetching data from a service or API.
  • Performing initialization tasks that depend on the component’s inputs or properties.

Example:

Press enter or click to view image in full size

Key Differences:

Press enter or click to view image in full size

Best Practice:

  • Use the constructor() for dependency injection and basic property initialization.
  • Use ngOnInit() for tasks that need to occur after the component’s inputs are available, such as fetching data, setting up observables, etc.

In summary, constructor() is for basic setup and dependency injection, while ngOnInit() is for initialization that needs to occur once the component is fully set up by Angular, including having its input properties initialized.

Slight Reframe:

In the more modern versions of Angular we're no longer using the constructor for dependency injection. We should be using the `inject` structure now.

Tuesday, March 25, 2025

Setting Up esLint for an Angular 19 Project

The angular-eslint package makes setting up linting in an Angular project pretty easy, but due to some recent changes there are a few new steps I had to work out to get it working so I figured I'd put them here for next time.

  1. Install the angular-eslint package: ng add angular-eslint
  2. Rename the generated eslint.config.js to eslint.config.mjs
  3. Change the 3 require statements at the top of the config file to import statements
    1. const eslint = require("@eslint/js"); => import esLint from '@eslint/js';
    2. const tseslint = require("typescript-eslint"); => import tsEsLint from 'typescript-eslint';
    3. const angular = require("angular-eslint"); => import angular from 'angular-eslint';
  4. Change the module.exports to export default
    1. module.exports = tseslint.config( => export default tsEsLint.config(
  5. In package.json, add a "type" setting with a value of "module"

At this point, esLint should be all setup. It actually works before any of this, but I wanted to use a plugin (more on that in a moment) and that required me to make these changes.

So, about that plugin. esLint made the decision in 8.53 to stop including stylistic rules as part of the linting process. I understand the decision, but I don't like it. Fortunately, there's a plugin for stylistic linting that you can add and it's pretty easy now that the above changes have been implemented.

  1. Install @stylistic/eslint-plugin-ts package: npm install @stylistic/eslint-plugin-ts
  2. Import the plugin in eslint.config.mjs
    1. import stylisticPlugin from '@stylistic/eslint-plugin-ts'
  3. In eslint.config.mjs, add a node as a sibling to the "files" node for plugins:
    plugins: {
          stylistic: stylisticPlugin
        },
  4.  Add the individual style rules to the rules node of eslint.config.mjs
    1. For example, to enfore the 1 true brace style rule, add "stylistic/brace-style": ["error", "1tbs"],

That's it! esLint should be all good to go. If you install a live linter extension in your working environment (like the esLint plugin for VS Code) you should see all your lint rules - including stylistic ones - applied.

Thursday, August 31, 2023

Beta Versioning Angular Libraries

Several years ago I worked for a company that was developing a distributed front-end application using Angular. We had issues getting everything to work together and I worked out some sort of solution to that problem using library versioning that was not semver. I'm currently working on a very similar problem (a distributed UI application built with Angular) and decided to try to figure out an actual good way of doing what I was trying to do back then. And I think I got it!

For starters, here's what I wrote up about that failed attempt all those years ago:

I was a contributor on a large application that used Angular for the front end. We decided that each piece would be developed separately by individual teams and then brought together as one massive monolithic application. One of the problems we encountered with this process was how we tested those individual packages prior to making them available for consumption in the application. We had more than one case where everything worked locally, but upon publication the whole application broke because of a small defect in one package. I designed a solution using VSTS (our build tool) to publish beta packages to our internal npm feed, then trigger a custom build of the application that consumed the newly published beta package. Using this new solution developers were able to test their published packages on a deployed test environment without changing the production ready packages and potentially crashing the application. Although this seems like something Semver could have handled (and was intended to handle), Semver was unfortunately not an option in our environment so we had to find a different solution.

Today's problem is pretty similar. We have multiple libraries that fully contain dedicated functionality and then a single presentation application that brings those libraries together. (I owe myself and you a separate blog post on creating a library in Angular that just works and is easy to change and validate before publishing, but this isn't that post.) For the sake of this article we're going to call the libraries tundra-ui-core, tundra-ui-payment, and tundra-ui-presentation.

tundra-ui-core

This library contains anything that is shared across two or more other libraries and/or tundra-ui-presentation.

tundra-ui-payment

This library contains all of the components, services, etc. responsible for accepting payments.

tundra-ui-presentation

This is the actual application, which will consume tundra-ui-core and tundra-ui-payment.

The Problem

As we work on tundra-ui-payment we're probably going to publish multiple versions of it to our internal feed to test it out within tundra-ui-presentation, but we don't want to waste a bunch of real version numbers doing that. We've decided we'll publish alpha versions for our developer testing and beta versions for our QA process. These will coincide with the branching strategy we already have in place for tundra-ui-presentation so that when a dev version of that project is published it automatically installs alpha versions of all libraries and when a QA version is published it automatically installs beta versions of all libraries.

Although this works, it created a major pain point because we had to manually increment the version of the library so that it included an alpha version, then change it to a beta version when we merged into the QA environment (I'll try to remember to document the branching strategy another time). It caused lots of extra commits with comments like "forgot to increment version" or "forgot to remove beta".

The Solution

It turns out that it's now possible to update the version of the library in the package.json file during the build process, then commit that change back into git without triggering another CI build. It took some doing to get things just right, but early testing is very promising and I couldn't wait to document it here.

I got started by following this guide, but I had to make some pretty significant changes so I'm going to document my whole process here. First off, we use Azure DevOps for our repositories, build and release pipelines, and to host our internal npm feed. We do not host our own instance of Azure DevOps so we do have access to the latest features.

Permissions

We're going to need two permissions setup in our Azure DevOps instance for each project that's going to host a repository that uses this method. If our Azure DevOps project is called Tundra then we'll need to make sure the associated user account for the Tundra project has these permissions. We'll also need to configure one of these permissions for each repository in each project. I know that's a pain, but the good news you only have to do it once per repository.

Open your project in Azure DevOps and navigate to Project Settings:


In the Project Settings pane, scroll down and select Repositories:


Choose the repository you want to add permissions for and navigate to the Security tab:


Scroll down to the Users section (expand it if necessary) and select the <Project> Build Service <Organization> user. For example, if our Tundra project was in the ArcticSoftware Organization we'd be looking for "Tundra Build Service (ArcticSoftware). Set the permissions below to "Allow" for this user.

  • Contribute

If you have branch policies active on any of the branches you'll be committing back to during the build process, you'll also need to enable these permissions:

  • Force push (rewrite history, delete branches and tags)
  • Bypass policies when pushing

The next step is granting permissions to the Artifacts feed to that same user account. In the left pane, choose Artifacts:

Click the settings cog near the upper right corner of the screen:

Navigate to the Permissions tab:

If you do not see the same user as above (Tundra Build Service (ArcticSoftware) in our example), click the "Add users/groups" button, select "Contributor", search for and select the user, click the "Save" button.

We now have the necessary permissions configured for this to work. We just have to create the build pipeline. I did this with yaml (I'm learning to love it) so I'm going to go through each step and explain what's happening and why I did it the way I did.

Build Pipeline

variables:
  isMain: $[eq(variables['Build.SourceBranch'], 'refs/heads/main')]
  isRelease: $[contains(variables['Build.SourceBranch'], 'release/')]
  isHotfix: $[contains(variables['Build.SourceBranch'], 'hotfix/')]
  isReadyForDeployment: $[eq(variables['Build.SourceBranch'], 'refs/heads/ready-for-deployment')]
  packageName: 'tundra/ui-payment'

We're setting up some variables we'll use in conditions later. The really basic explanation of our branching strategy is that we have main, ready-for-deployment (which is a holding branch for the period between the end of our sprint and the time the code gets deployed to production), release/* and hotfix/* branches for things that are in QA, and then everything else (story and task branches basically).

- checkout: self
  persistCredentials: true
  clean: true
  fetchDepth: 0
  lfs: true
  submodules: recursive

We have to checkout the repository. This is a key step that I initially overlooked and it caused me some problems later. You can find out more about the options specified here on your own. This is what I needed.

- task: Npm@1
  displayName: 'install dependencies'
  inputs:
    command: install
    verbose: false

We have an .npmrc file setup for our internal feed that's in the same director as this .yaml file so we don't need to specify any credentials or anything here to install dependencies (remember that we setup permissions on our internal feed on an earlier step).

- task: npmAuthenticate@0
  displayName: 'authenticate with internal npm feed'
  inputs:
    workingFile: '.npmrc'

This was another step that caused a headache for me. The native npm install command will authenticate internally, but in order to access the npm feed through Powershell (which we'll do in the next step) we have to authenticate explicitly, which modifies the .npmrc file.

- powershell: |
   $sourceBranch = "$(Build.SourceBranch)".replace("refs/heads/", "")
   $packageJson = Get-Content "./projects/tundra/**/package.json" | ConvertFrom-Json
   $baseVersion = $packageJson.version.split("-")[0];
   $allVersions = npm view $packageJson.name versions --json --silent
   $alphaVersions = ($allVersions | where{$_ -like "*" + $baseVersion + "-alpha*"})
   $newSubversion = 1
   if ($alphaVersions -ne $null) {
    $alphaVersions = $alphaVersions.trim()
    $lastAlphaVersion = 0
    if ($alphaVersions[1].length -eq 1) {
      $lastAlphaVersion = $alphaVersions.split(".")[3].replace("""", "").replace(",", "")
    } else {
      $lastAlphaVersion = $alphaVersions[$alphaVersions.length - 1].split(".")[3].replace("""", "")
    }
    $newSubversion = [int]$lastAlphaVersion + 1
   }
   $newVersion = $baseVersion + "-alpha." + $newSubversion
   if ($newVersion -ne $packageJson.version) {
    $packageJson.version = $newVersion
    git checkout $sourceBranch --quiet
    git config --global user.email "BuildPipeline@arcticsoftware.com"
    git config --global user.name "Build Pipeline"
    ConvertTo-Json $packageJson -Depth 2 | Out-File "./projects/tundra/**/package.json"
    git restore .npmrc
    git commit -am "[skip ci] Pipeline Modification: Package version: $newVersion"
   } else {
    echo "The new version would have been the same as the current version, so nothing was changed."
    exit 0
   }
  displayName: 'Append -alpha.<alpha version> to version number'
  condition: and(ne(variables.isMain, true), ne(variables.isRelease, true), ne(variables.isHotfix, true), ne(variables.isReadyForDeployment, true))

This is where the heavy lifting starts. This is a pretty big Powershell script (obviously) that's doing a lot of stuff. I'm going to break this one down pretty much line-by-line so it makes more sense. 

$sourceBranch = "$(Build.SourceBranch)".replace("refs/heads/", "")

The built-in variables Azure DevOps gives us are great, but they weren't quite what we needed for a future step. We want to checkout the branch that triggered this build, but if that branch is in a folder we need to specify the folder and the branch name. If we use $(Build.SourceBranchName) we don't get the folder and if we use $(Build.SourceBranch) we get the folder and branch name, but also "refs/heads/". All we're doing here is getting the folder and branch name and saving it for later.

$packageJson = Get-Content "./projects/connect/**/package.json" | ConvertFrom-Json

We're using a couple of cmdlets available to Powershell to store the contents of the package.json file in a variable so we can find the version number and manipulate it.

$baseVersion = $packageJson.version.split("-")[0];

We want to isolate the base package version and this line does that. We use Semver so by "base package version" I mean if the value in the version field is "1.0.1-alpha.1" we just want the "1.0.1" part saved for later. By splitting the entire value on the "-" and taking the first part, we get just that.

$allVersions = npm view $packageJson.name versions --json --silent

This is possibly the coolest part of the whole thing. We're going to our internal feed and retrieving every version that's ever been published of this specific package (tundra-ui-payment). The --json switch saves the value as JSON and the --silent switch just prevents the pipeline from displaying error messages if the package doesn't exist at all.

The view versions command appears to return the packages in publication order, which is exactly what we want. However, I can't find any confirmation that will always be the case so this is a potential spot for an issue to arise.

$alphaVersions = ($allVersions | where{$_ -like "*" + $baseVersion + "-alpha*"})

Once we have all of the versions, we need to filter that list to get only the alpha versions of this base version. If our base version is 1.0.1 then we want to get all of the versions that are "1.0.1-alpha*" where the * is a wildcard standing in for any number of characters. This will return "1.0.1-alpha.1", "1.0.1-alpha.sigma" or anything else that matches the pattern (though we expect it to always be in the format "1.0.1-alpha.<number>").

$newSubversion = 1
   if ($alphaVersions -ne $null) {
    $alphaVersions = $alphaVersions.trim()
    $lastAlphaVersion = 0
    if ($alphaVersions[1].length -eq 1) {
      $lastAlphaVersion = $alphaVersions.split(".")[3].replace("""", "").replace(",", "")
    } else {
      $lastAlphaVersion = $alphaVersions[$alphaVersions.length - 1].split(".")[3].replace("""", "")
    }
    $newSubversion = [int]$lastAlphaVersion + 1
   }

The next several steps all go together and are pretty dumb to have to do, honestly. First we're creating a new variable ($newSubversion) and initializing it to 1. Then we're going to try to figure out what the last alpha version was that was published to our feed. Unfortunately, the where cmdlet of Powershell sometimes returns an array and sometimes returns a single value. If there are multiple matches we'll get back an array containing all matches, but if there's only one match then we get back just that value as a string (which is also an array of characters).

If we try to split on the "." character and there was only one result we'll end up with either an error or useless data. I don't remember which, but it wasn't right. That's where the second if statement comes into play. (The first if statement just checks whether we got any matches; that is: whether there are currently any alpha versions that match this semver version.) We're checking the length of the second index in the array. If the "array" is an array of characters then the length of the second index will be one (because a character has a length of one), but if the "array" is actually an array of versions then the length of the second index will be longer. Either way we want to get the very last part of the version so we know what the next subversion should be. If the last version published was 1.0.1-alpha.3 then we want to isolate the 3 so we can increment it to 4 and make the next version 1.0.1-alpha.4.

$newVersion = $baseVersion + "-alpha." + $newSubversion

We're combining the base version with "-alpha." and the new subversion number to get the full new alpha version number.

if ($newVersion -ne $packageJson.version) {

We actually have to check whether the version has changed because there's a possibility it hasn't. For example, if the previous build didn't publish the package to the feed successfully then the version won't change this time around. If we just proceed as though the version has changed we actually end up with an error and our build fails.

$packageJson.version = $newVersion

This is changing the value in the JSON representation of the package.json that we still have saved in memory (and which we'll write back to disk in a future step).

git checkout $sourceBranch --quiet

Remember that branch variable from earlier? This is where we use it. Even though this pipeline was triggered by a specific branch, we have to checkout the branch so we can commit the modified package.json file back to it.

git config --global user.email "BuildPipeline@arcticsoftware.com"
git config --global user.name "Build Pipeline"

git needs to know a little about "us" in order to allow us to commit our changes. You can use whatever values you want right here, but you have to do this.

ConvertTo-Json $packageJson -Depth 2 | Out-File "./projects/connect/**/package.json"

Just like we used cmdlets to get the contents of the package.json file into memory, we're dumping the updated JSON back into the file. I don't know what the -Depth option does. I just know that 2 works for us.

git restore .npmrc

We want to revert the changes to the .npmrc file because it was modified earlier to include a key that we don't want to commit to git. This is a weird way to do this, but it's the only way that worked. I originally tried to just stage package.json, but that command failed when it ran in Azure DevOps. It was weird. This works, though. The only files that should have changed are .npmrc and package.json so just restoring .npmrc means we can commit everything else.

git commit -am "[skip ci] Pipeline Modification: Package version: $newVersion"

This is where that happens. We're staging and committing all of the remaining modified files in one command. The key part of this comment is "[skip ci]" which tells Azure DevOps not to trigger a build pipeline for this commit. If we don't include that, our changes will just keep triggering new builds forever (or until Azure DevOps gets tired of our shenanigans and quits).

} else {
    echo "The new version would have been the same as the current version, so nothing was changed."
    exit 0
   }

Finally, we have to exit the Powershell script with a success code (0) if the version hasn't changed. If we don't do this, the whole build will fail. 

condition: and(ne(variables.isMain, true), ne(variables.isRelease, true), ne(variables.isHotfix, true), ne(variables.isReadyForDeployment, true))

This condition just specifies that this Powershell script should only run when the source (triggering) branch is not main, ready-for-deployment, release/*, or hotfix/*. In other words, only do this step for story or task branches.

Man, that was a lot. It took me a while to get all the nuances figured out. There's a nearly duplicate script that runs for the beta builds. I'm not going to put it here because it really is nearly identical to this version. Just picture all the references to "alpha" changed to "beta" and the condition requiring the branch to be release/* or hotfix/*.

If the source branch is ready-for-deployment, however, we want to strip off the "alpha" and "beta" parts of the package, which does require a different Powershell script.

- powershell: |
   $sourceBranch = "$(Build.SourceBranch)".replace("refs/heads/", "")
   $packageJson = Get-Content "./projects/tundra/**/package.json" | ConvertFrom-Json
   git config --global user.email "BuildPipeline@arcticsoftware.com"
   git config --global user.name "Build Pipeline"
   git checkout $sourceBranch --quiet
   $packageJson.version = $packageJson.version.split("-")[0];
   ConvertTo-Json $packageJson -Depth 2 | Out-File "./projects/tundra/**/package.json"
   git restore .npmrc
   git commit -am "[skip ci] Pipeline Modification: Package version: $newVersion"
  displayName: 'Remove -alpha.<alpha version> and -beta.<beta version> from version number'
  condition: eq(variables.isReadyForDeployment, true)

A lot of this is the same so it doesn't warrant a line-by-line explanation. The big difference here is that instead of building a new version/subversion number, we're just writing the base version as the version. If the merge had "1.0.1-alpha.19" as the version, this will replace that with just "1.0.1".

Before we publish the package to the internal feed (I've omitted the build step, but you'll see it in the full file I post at the end) we want to push our changes back to the source branch of the triggering repository. That's one more Powershell script.

- powershell: |
   $sourceBranch = "$(Build.SourceBranch)".replace("refs/heads/", "")
   git checkout $sourceBranch --quiet
   git push https://$(System.AccessToken)@dev.azure.com/arcticsoftware/Tundra/_git/$(Build.Repository.Name) -q -f
  displayName: 'Push the merged source branch back into the repository without triggering another run of the pipeline'
  condition: and(succeeded(), ne(variables.isMain, true))

We have to get the source branch again and check it out again. That's just to make sure we're on the branch in case the earlier step didn't end up changing the version. The key piece is the git push line, which uses another built-in variable ($(System.AccessToken)) to authenticate to the repository. This is why we had to add the Contribute permission earlier. I've specified the -f (force) flag here because we have branch policies on some of these branches that we need to override.

- task: Npm@1
  condition: and(succeeded(), ne(variables.isMain, true), ne(variables.isRelease, true), ne(variables.isHotfix, true), ne(variables.isReadyForDeployment, true))
  displayName: 'publish the library to the internal npm feed with the alpha tag'
  inputs:
    command: custom
    workingDir: '$(Build.Repository.LocalPath)/dist/$(packageName)'
    verbose: false
    customCommand: 'publish --tag alpha'
    customRegistry: useFeed
    customFeed: 'this will be your custom feed ID'

This is really the last relevant step. There are three of these tasks, each with different conditions and tags. We're publishing our previously built package to our internal feed. Once again we have a condition to execute this step for all branches that are not main, ready-for-deployment, release/*, or hotfix/*. You can see on the customCommand line that we're using the --tag switch of the npm publish command to tag this package with "alpha". This enables us to install this version of the package in tundra-ui-presentation without overwriting the production-ready tundra-ui-payment package. The other two blocks replace "alpha" with "beta" and "latest", which is a keyword used by npm to identify the latest live version of the package.

The other half of this is that our tundra-ui-presentation build pipeline installs packages tagged as alpha whenever a task or story branch is built, installs packages tagged as beta whenever a release/* or hotfix/* branch is built. Otherwise, it installs the latest packages. As long as we're using semver correctly and allowing npm to get updated minor or patch versions, we shouldn't have to modify tundra-ui-presentation just to get an updated version of a library.

Here's the whole file. I did all the work so you (or future me) don't have to!

pool:
  name: Azure Pipelines
  vmImage: 'ubuntu-latest'

variables:
  isMain: $[eq(variables['Build.SourceBranch'], 'refs/heads/main')]
  isRelease: $[contains(variables['Build.SourceBranch'], 'release/')]
  isHotfix: $[contains(variables['Build.SourceBranch'], 'hotfix/')]
  isReadyForDeployment: $[eq(variables['Build.SourceBranch'], 'refs/heads/ready-for-deployment')]
  packageName: 'tundra/ui-payment'

# Trigger the pipeline for every branch
trigger:
  - main
  - release/*
  - hotfix/*
  - ready-for-deployment

steps:
- checkout: self
  persistCredentials: true
  clean: true
  fetchDepth: 0
  lfs: true
  submodules: recursive

- task: NodeTool@0
  displayName: 'Use Node 14.x'
  inputs:
    versionSpec: 14.x

- task: Npm@1
  displayName: 'install dependencies'
  inputs:
    command: install
    verbose: false

- task: npmAuthenticate@0
  displayName: 'authenticate with internal npm feed'
  inputs:
    workingFile: '.npmrc'

# 1. Get the current contents of package.json of the library
# 2. Find the latest alpha package on the feed with this version number (e.g. 1.1.0-alpha.*)
# 3. If this package is not on the feed or there are no packages on the feed with this version number and "alpha", use 1 as the alpha version
# 4. If there is a package with this version number and "alpha", get the highest alpha version and use the next number as this alpha version
# 5. If the new alpha version will be the same as the old version, do nothing and exit
# 6. If the new alpha version will be different from the old version, save the new version and write the updated JSON back to the package.json file
# 7. Commit the changes back to the branch that triggered the pipeline
# Note: This step applies to all branches that are not: main, ready-for-deployment, release/*, or hotfix/*
- powershell: |
   $sourceBranch = "$(Build.SourceBranch)".replace("refs/heads/", "")
   $packageJson = Get-Content "./projects/tundra/**/package.json" | ConvertFrom-Json
   $baseVersion = $packageJson.version.split("-")[0];
   $allVersions = npm view $packageJson.name versions --json --silent
   $alphaVersions = ($allVersions | where{$_ -like "*" + $baseVersion + "-alpha*"})
   $newSubversion = 1
   if ($alphaVersions -ne $null) {
    $alphaVersions = $alphaVersions.trim()
    $lastAlphaVersion = 0
    if ($alphaVersions[1].length -eq 1) {
      $lastAlphaVersion = $alphaVersions.split(".")[3].replace("""", "").replace(",", "")
    } else {
      $lastAlphaVersion = $alphaVersions[$alphaVersions.length - 1].split(".")[3].replace("""", "")
    }
    $newSubversion = [int]$lastAlphaVersion + 1
   }
   $newVersion = $baseVersion + "-alpha." + $newSubversion
   if ($newVersion -ne $packageJson.version) {
    $packageJson.version = $newVersion
    git checkout $sourceBranch --quiet
    git config --global user.email "BuildPipeline@arcticsoftware.com"
    git config --global user.name "Build Pipeline"
    ConvertTo-Json $packageJson -Depth 2 | Out-File "./projects/tundra/**/package.json"
    git restore .npmrc
    git commit -am "[skip ci] Pipeline Modification: Package version: $newVersion"
   } else {
    echo "The new version would have been the same as the current version, so nothing was changed."
    exit 0
   }
  displayName: 'Append -alpha.<alpha version> to version number'
  condition: and(ne(variables.isMain, true), ne(variables.isRelease, true), ne(variables.isHotfix, true), ne(variables.isReadyForDeployment, true))

# 1. Get the current contents of package.json of the library
# 2. Find the latest beta package on the feed with this version number (e.g. 1.1.0-beta.*)
# 3. If this package is not on the feed or there are no packages on the feed with this version number and "beta", use 1 as the beta version
# 4. If there is a package with this version number and "beta", get the highest beta version and use the next number as this beta version
# 5. If the new beta version will be the same as the old version, do nothing and exit
# 6. If the new beta version will be different from the old version, save the new version and write the updated JSON back to the package.json file
# 7. Commit the changes back to the branch that triggered the pipeline
# Note: This step applies to all release/* and hotfix/* branches
- powershell: |
   $sourceBranch = "$(Build.SourceBranch)".replace("refs/heads/", "")
   $packageJson = Get-Content "./projects/tundra/**/package.json" | ConvertFrom-Json
   $baseVersion = $packageJson.version.split("-")[0];
   $allVersions = npm view $packageJson.name versions --json
   $betaVersions = ($allVersions | where{$_ -like "*" + $baseVersion + "-beta*"})
   $newSubversion = 1
   if ($betaVersions -ne $null) {
    $betaVersions = $betaVersions.trim()
    $lastBetaVersion = 0
    if ($betaVersions[1].length -eq 1) {
      $lastBetaVersion = $betaVersions.split(".")[3].replace("""", "").replace(",", "")
    } else {
      $lastBetaVersion = $betaVersions[$betaVersions.length - 1].split(".")[3].replace("""", "")
    }
    $newSubversion = [int]$lastBetaVersion + 1
   }
   $newVersion = $baseVersion + "-beta." + $newSubversion
   if ($newVersion -ne $packageJson.version) {
    $packageJson.version = $newVersion
    git checkout $sourceBranch --quiet
    git config --global user.email "BuildPipeline@arcticsoftware.com"
    git config --global user.name "Build Pipeline"
    ConvertTo-Json $packageJson -Depth 2 | Out-File "./projects/tundra/**/package.json"
    git restore .npmrc
    git commit -am "[skip ci] Pipeline Modification: Package version: $newVersion"
   } else {
    echo "The new version would have been the same as the current version, so nothing was changed."
    exit 0
   }
  displayName: 'Append -beta.<beta version> to version number'
  condition: or(eq(variables.isRelease, true), eq(variables.isHotfix, true))

# 1. Get the current contents of package.json of the library
# 2. Create a new local branch using the build number
# 3. Remove "-alpha.<subversion>" and "-beta.<subversion>" from the version number
# 4. Reset the subversion to 1
# 5. Write the updated JSON back to the package.json file
# 6. Commit the changes back to the local branch created in step 2
# Note: This step only applies to the ready-for-deployment branch
- powershell: |
   $sourceBranch = "$(Build.SourceBranch)".replace("refs/heads/", "")
   $packageJson = Get-Content "./projects/tundra/**/package.json" | ConvertFrom-Json
   git config --global user.email "BuildPipeline@arcticsoftware.com"
   git config --global user.name "Build Pipeline"
   git checkout $sourceBranch --quiet
   $packageJson.version = $packageJson.version.split("-")[0];
   ConvertTo-Json $packageJson -Depth 2 | Out-File "./projects/tundra/**/package.json"
   git restore .npmrc
   git commit -am "[skip ci] Pipeline Modification: Package version: $newVersion"
  displayName: 'Remove -alpha.<alpha version> and -beta.<beta version> from version number'
  condition: eq(variables.isReadyForDeployment, true)

- task: Npm@1
  displayName: 'build library with production configuration'
  inputs:
    command: custom
    verbose: false
    customCommand: 'run lib:build -- --configuration=production'

# 1. Switch to the source branch of this build
# 2. Push the updated source branch back into the repository without triggering another pipeline ([skip ci])
# Note: This step applies to all branches except main
- powershell: |
   $sourceBranch = "$(Build.SourceBranch)".replace("refs/heads/", "")
   git checkout $sourceBranch --quiet
   git push https://$(System.AccessToken)@dev.azure.com/arcticsoftware/Tundra/_git/$(Build.Repository.Name) -q -f
  displayName: 'Push the merged source branch back into the repository without triggering another run of the pipeline'
  condition: and(succeeded(), ne(variables.isMain, true))

# Publish the alpha library to the internal npm feed
- task: Npm@1
  condition: and(succeeded(), ne(variables.isMain, true), ne(variables.isRelease, true), ne(variables.isHotfix, true), ne(variables.isReadyForDeployment, true))
  displayName: 'publish the library to the internal npm feed with the alpha tag'
  inputs:
    command: custom
    workingDir: '$(Build.Repository.LocalPath)/dist/$(packageName)'
    verbose: false
    customCommand: 'publish --tag alpha'
    customRegistry: useFeed
    customFeed: 'this will be your custom feed ID'

# Publish the beta library to the internal npm feed
- task: Npm@1
  condition: and(succeeded(), or(eq(variables.isRelease, true), eq(variables.isHotfix, true)))
  displayName: 'publish the library to the internal npm feed with the beta tag'
  inputs:
    command: custom
    workingDir: '$(Build.Repository.LocalPath)/dist/$(packageName)'
    verbose: false
    customCommand: 'publish --tag beta'
    customRegistry: useFeed
    customFeed: 'this will be your custom feed ID'

  # Publish the latest library to the internal npm feed
- task: Npm@1
  condition: and(succeeded(), eq(variables.isMain, true))
  displayName: 'publish the library to the internal npm feed with the latest tag'
  inputs:
    command: custom
    workingDir: '$(Build.Repository.LocalPath)/dist/$(packageName)'
    verbose: false
    customCommand: 'publish --tag latest'
    customRegistry: useFeed
    customFeed: 'this will be your custom feed ID'








Monday, April 17, 2023

Unit Test and Code Coverage Reports with Angular

I recently endeavored to have code coverage available as a metric in Azure DevOps for my Angular application. It turned out to be a little bit (but not much) trickier than I thought it would be so here I am.

I started with this two part article on how to get started, but had to make a few tweaks. You should go read those posts if they're still up, then come back here and skip down to the heading "The Changes".

The Summary

The first thing we have to do is get our test results generated locally. The linked posts above use junit and that met my needs so that's what I'm doing as well.

  1. Install the junit karma reporter: npm install karma-junit-reporter --save-dev
  2. Configure Karma to require junit
    • In karma.conf.js, add require('karma-junit-reporter') to the plugins array (which should already require things like karma-jasmine)
  3. Configure Karma to use junit as a reporter
    • In karma.conf.js, add 'junit' to the reporters array (which should already include 'progress' and 'kjhtml')
  4. Configure the junit reporter within Karma
    • In karma.conf.js, add the following configuration as a sibling of the reporters array in the previous step:
      •         junitReporter: {
          outputDir: 'reports/junit',
          outputFile: 'unit-test-results.xml',
          useBrowserName: false
        }
        		
        		
    • This configuration uses junit to create a file named unit-test-results.xml in a folder named reports/junit as a sibling of the src folder
    • Change the values of outputDir and outputFile to use a different file/folder combination
  5. Make sure Karma is configured to use a headless browser
    • The browsers array accepts ChromeHeadless as a value (it also accepts FirefoxHeadless, but I've had issues with headless Firefox holding on to resources and crashing my system so I tend to avoid it)
  6. We can now run ng test --watch=false to run all of our unit tests once and generate test results

Now that we have test results in an XML file we want to add code coverage results as well. We're going to use karma-coverage to do that part.

  1. Install the karma coverage reporter: npm install karma-coverage --save-dev
  2. Configure Karma to require karma coverage
    • In karma.conf.js, add require('karma-coverage') to the plugins array (the same array from step 2 in the previous section)
  3. Configure Karma to use karma coverage as a reporter
    • In karma.conf.js, add 'coverage' to the reporters array
  4. Configure the karma coverage reporter the way you want it
    • When you use the Angular CLI, karma-coverage is actually configured by default, but you may want to change the values
    • You can see the default configuration by looking for the coverageReporter node that is a sibling of the reporters array
    • I changed mine to output the file in the same reporters folder as the unit test results (reports/), in a subfolder called coverage
      • dir: require('path').join(__dirname, './reports/coverage'),
    • I also removed the text-summary reporter configuration and added the cobertura reporter configuration, specifying the file name to be code-coverage.xml
      • coverageReporter: {
          dir: require('path').join(__dirname, './reports/coverage'),
          subdir: '.',
          reporters: [
            { type: 'html' },
            {
              type: 'cobertura',
              file: 'code-coverage.xml'
            }
          ]
        }
        
        
  5. We can now run ng test --watch=false --code-coverage to run all of our unit tests once and generate test results and code coverage results
  6. The last thing we need to do is exclude files from code coverage that we don't want to... well, include
    • In angular.json, locate the projects/<project name>/architect/test node and add a codeCoverageExclude property, which is an array of strings
      • In the array, specify each fully qualified path - starting with src - to exclude from code coverage

That's it! Just like that, we have unit tests and code coverage results, but now we want to display those results in Azure DevOps. It turns out that's pretty easy, too.

  1. In your package.json, create a script to run your tests without watching and with code coverage results generated, to make it easier to do this from your build
    • "test-code-coverage": "ng test --watch=false --code-coverage"
  2. Create a new build (or add a test step to your current build, but I prefer to run my unit tests when a pull request is created rather than while I'm trying to deploy something)
    • pool:
        name: Azure Pipelines
      steps:
        - task: Npm@1
          displayName: 'npm install'
          inputs:
            verbose: false
        
        - task: Npm@1
          displayName: 'Execute tests'
          input:
            command: custom
            verbose: false
            customCommand: 'run test-code-coverage'
            
        - task: PublishTestResults@1
          displayName: 'Publish Test Results - Generate Test Report'
          inputs:
            testResultsFiles: '**/reports/junit/unit-test-results.xml'
            
        - task: PublishCodeCoverageResults@1
          displayName: 'Unit Test Code Coverage Report'
          inputs:
            codeCoverageTool: Cobertura
            summaryFileLocation: '**/reports/coverage/code-coverage.xml'
      
  3. As long as you publish your test results to the pipeline (as in the sample YAML above), Azure DevOps will automatically pick up the results and display them in a nice little UI for you

Friday, July 22, 2022

Dynamic QueryParams with RouterLink

This is something that should have been obvious, but still required me to find an answer on SO. As I do, I'm putting it here to make it easier to find in the future. Basically I wanted to use an anchor tag with the routerLink directive, but send out dynamic parameters and I couldn't figure out how to do it. It turns out you can just bind the queryParams part of routerLink to a property on your component and pass it that way.

component.ts

<snip>...<snip>
public dynamicQueryParams: Params;
<snip>...<snip>
this.dynamicQueryParams = {first: 'value', second: 'empty'};
<snip>...<snip>

component.html

<snip>...<snip>
<a [routerLink]="['/support/case-history']" [queryParams]="dynamicQueryParams" target="_blank">Click Me!</a>
<snip>...<snip>
That's pretty much it. Obviously, you'll need to build your dynamicQueryParams variable the way you want it, but this is how you can pass dynamic parameters via a routerLink directive.

Monday, July 18, 2022

No value accessor for form control with name

 I'm going to be brief here and hopefully circle back to this with more details later. As of today (July 18, 2022) there is an open issue in the Angular Github repository stating the team should try to improve this error message. I wish they would because today I wasted two hours on something really stupid. Ultimately, it was my fault for doing the wrong thing, but the whole point of error messages should be to help you find and fix the error.

Anyway, what happened to me today is I was using a 3rd party component (Kolkov Angular Editor) and it was throwing an error that originally said "No value accessor for form control with unspecified name attribute". After some trial and error and moving a bunch of stuff around I thought I had identified that the problem was caused by the 3rd party control itself and there was some bug in it. I applied a workaround that fixed it in one place, but not the other. Eventually, I was able to piece together that I was importing the 3rd party module in my wrong module.

My app is broken up (as it should be) into separate modules by purpose. Then I have one SharedModule where I declare, import, and export anything that's used across multiple modules in my app. When I originally built the piece that uses the Kolkov Angular Editor I had it all in one module, but as my app grew I moved some of my components into my SharedModule, but left the importation of the Kolkov module in its original place. That was the root of my problem. Once I moved the import (and export) into my SharedModule everything worked perfectly. And it only took me two hours to figure out.

Monday, May 23, 2022

No provider for ControlContainer!

As you can probably deduce from the title, this post is all about an annoying error in Angular that reads "No provider for ControlContainer!". If you're just getting this error in general, the first thing you should do is make sure you've imported the FormsModule and ReactiveFormsModule (yes, both of them). That will probably clear it up for you. If you've done that and you're still getting this error, specifically while running your unit tests, I have a solution for you. I'm nearly 100% certain I didn't come up with this on my own, but since I'm not sure where I originally found it I can't link you over to it. Sorry for that.

First off, let me show you the code that triggered this issue when I started testing. Like a good programmer following DRY (Don't Repeat Yourself) I will often componentize even small things that are reused and require some bit of setup or configuration. For instance, when creating forms that require masked input fields (like phone number or credit card) I'll create a component that allows me to quickly and easily drop that into my form and just specify which mask to use. The way I do that requires me to inject ControlContainer directly into the component. Here's some example code:

import { Component, Input, OnInit } from '@angular/core';
import { ControlContainer, FormControl, FormGroup } from '@angular/forms';

@Component({
  selector: 'app-masked-input'
  templateUrl: './masked-input.html'
})
export class MaskedInputComponent implements OnInit {
  @Input() controlName: string;
  @Input() label: string;
  @Input() mask: any;

  constructor(public controlContainer: ControlContainer) { }

  public ngOnInit(): void {
    // Set our form property to the parent control
    // (i.e. FormGroup) that was passed to us, so that our
    // view can data bind to it
    this.form = this.controlContainer.control as FormGroup;
    this.control = this.form.get(this.controlName) as FormControl;
  }
}

That's the relevant part to this error. If we try to run unit tests around that code we'll get the error "No provider for ControlContainer!" even if we import FormsModule and ReactiveFormsModule in our test file. To fix that error, we have to manually create a FormGroupDirective in our test file and change the provider for ControlContainer to use that FormGroupDirective. Here are the relevant bits of code:

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ControlContainer, FormControl, FormGroup, FormGroupDirective, FormsModule, ReactiveFormsModule } from '@angular/forms';
...snip...
describe('MaskedInputComponent'), () => {
  let component: MaskedInputComponent;
  let fixture: ComponentFixture<MaskedInputComponent>;
  const formGroup: FormGroup = new FormGroup({
    dynamicControlName: new FormControl('')
  });
  const formGroupDirective: FormGroupDirective = new FormGroupDirective([], []);
  formGroupDirective.form = formGroup;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [ MaskedInputComponent ],
      imports: [ FormsModule, ReactiveFormsModule ],
      providers: [ { provide: ControlContainer, useValue: formGroupDirective } ],
    })
    compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(MaskedInputComponent);
      component = fixture.componentInstance;
      component.controlName = 'dynamicControlName';
      fixture.detectChanges();
    })
    compileComponents();
  });
});

That's it! This will alleviate the "No provider for ControlContainer!" error in your test run. It's simple enough once you know what to do, but it was a pain figuring it out. Hope this helps.