Enforcing front-end guidelines in an Angular project
Tags: angular, frontend, vscode, productivity
Published: 2023-11-14 09:36:46

The title of this blog might be a little deceiving, because only a part of it can actually be seen as enforcing guidelines. It’s more about how to make team members work more in sync with each other, making sure everyone is coding more or less the same, like a tiny hive mind. Having those pull requests in which no one has to make comments like “no excessive empty lines”, “remove unused imports” or “clean up console.logs”. 


To try to achieve this, we can set up the Angular project in such a way that team members are more or less forced to write and commit code as the project demands it. Here are a few tips and tricks for setting up an Angular project to help your team be more in sync.


## 1. Communication


It's actually a no-brainer, but I see a lack of communication too often; that's why it comes first. The first thing you should do is ask other team members. Especially if you're new to the team, ask:


- where documentation can be found.
- whether you need specific access.
- if there are any guidelines to be followed.
- whether someone can go through the project with you (project structure, special things, etc.).
- what the rules and guidelines are for committing or pushing code.
- …


## 2. Visual Studio code


You can’t force your team members to use a certain IDE, but Visual Studio Code is free and provides a good development experience. If your team members all share the same IDE, it’s better they share a common set of settings, extensions,...


### Recommended extensions


Update or add `extensions.json` in the `.vscode` folder. This way, all team members are going to use the same set of extensions that are required for the project.


> Note: VS Code only suggests downloading these recommended extensions; it is still up to the team member to install them (however recommended)!


```JSON
{
  // For more information, visit: <https://go.microsoft.com/fwlink/?linkid=827846>
  "recommendations": [
    "angular.ng-template",
    "johnpapa.angular-essentials",
    "streetsidesoftware.code-spell-checker",
    "yzhang.markdown-all-in-one",
    "stylelint.vscode-stylelint"
  ]
}
```


These are my five recommended extensions:


- Angular Language Service (angular.ng-template): provides a rich editing experience for Angular templates, both inline and external templates.
- Angular Essentials (johnpapa.angular-essentials): all the essential extensions needed for Angular development.
- Code Spell Checker (streetsidesoftware.code-spell-checker): make sure any spelling errors are visible and can be caught.
- Markdown All in One (yzhang.markdown-all-in-one): for any markdown related matters. Especially useful for previewing those README.md’s!
- Stylelint (stylelint.vscode-stylelint): immediately see stylelint errors in VS Code. If you're using SCSS files, be sure to add it to the language ids to be validated in the settings of the Stylelint extension.


### Project settings


Add a `settings.json` file to the project (under the `.vscode` folder) that will use project specific settings in Visual Studio Code. This is the `settings.json` I usually use (depending on the project):


```JSON
{
  "editor.codeActionsOnSave": {
    "source.organizeImports": true
  },
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.formatOnSave": true,
  "editor.lineNumbers": "on",
  "editor.trimAutoWhitespace": true,
  "editor.tabSize": 2
}
```


These settings will:


- sort imports and remove unused imports on save.
- use the Prettier extension as the default formatter when formatting a file.
- format the file on save.
- show line numbers.
- automatically trim whitespace.
- set the editor tab size to 2 spaces.


> Note: Prettier should be installed as part of the Angular Essentials extension pack.


### Add project snippets


Add snippets to your project that help people write code faster and more efficiently, but in a specific way. You can do so by creating a snippets file in the `.vscode` folder. You can choose the name, as long as it ends with the extension `.code-snippets`. An example snippet may be a snippet for a *Given-When-Then* code block for unit tests:


```JSON
{
  "Given-When-Then": {
  "scope": "typescript",
  "prefix": "gwt",
  "body": [
   "describe('$0', () => {",
      "\tbeforeEach(() => {",
   "\t}",
      "\tdescribe('$1', () => {",
      "\t\tit('$2', () => {",
   "\t\t});",
   "\t});",
   "});"
  ],
  "description": "Create a Given-When-Then block"
 }
}
```


There are many extensions that provide snippets out of the box. Have a look in the extensions store of VS Code and update each team member to make use of these snippets.


## 3. Prettier


Prettier is a code formatter that formats your code to a uniform, consistent style. That way, the code looks the same for everyone using the same Prettier settings.


> As stated earlier, the Prettier extension for VS Code itself should already be installed by the Angular Essentials extension pack.


For formatting inside the project, install Prettier in the project via NPM:


```Bash
npm install prettier -D
```


Create a `.prettierrc.json` file in the root of the project that will contain the Prettier configuration. You can use the default that came installed with Prettier or use this one (*note*: make sure the tabWidth is the same as the one you defined in the `settings.json`!):


```JSON
{
  "tabWidth": 2,
  "useTabs": false,
  "singleQuote": true,
  "semi": true,
  "bracketSpacing": true,
  "arrowParens": "avoid",
  "trailingComma": "es5",
  "bracketSameLine": true,
  "printWidth": 80,
  "endOfLine": "auto"
}
```


What’s the difference with writing the configuration in here instead of in `settings.json` (e.g., `“prettier.tabWidth”: 2`)? It’s essentially the same; when you format the file, it will look for the Prettier configuration, but the `.prettierrc.json` will take precedence over the settings in `settings.json`. I like using a separate `.prettierrc.json` file because 1) it clearly shows the Prettier configuration in one file, and 2) if the team would step away from VS Code, the Prettier configuration can be reused.


Add a `.prettierignore` file (recommended by Prettier itself) that contains the same content as the .gitignore (unless you have some exceptions of your own).


## 4. Linting with ESlint


Linting is a process that will check your code for potential errors. Quite essential in my opinion! Not only can you check for potential errors, but you can also enforce a certain set of rules (guidelines) with linting.


Add linting for the Angular project:


```Bash
ng add @angular-eslint/schematics
```


This will install the `@angular-eslint/schematics` package from NPM and will create an `.eslintrc.json` file that will contain the linting configuration. It will also update your `angular.json` with a new architect target “lint” and create a new script line in your `package.json` called “lint”.


With linting rules, you can enforce a common set of coding standards. When running the `@angular-esling/schematics` schematic, it will include a `.eslintrc.json` file in your project. It will already extend some rules for ts and html files from the recommended ones. For TypeScript files:


- `eslint:recommended` is ESLint's inbuilt "recommended" configuration; it turns on a small, sensible set of rules that lint for well-known best practices. See https://eslint.org/docs/latest/rules/
- `plugin:@typescript-eslint/recommended` is similar to `eslint:recommended`, except it turns on TypeScript-specific rules. See https://typescript-eslint.io/rules/
- `plugin:@angular-eslint/recommended`, see https://github.com/angular-eslint/angular-eslint/blob/main/packages/eslint-plugin/src/configs/README.md#recommended
- `plugin:@angular-eslint/template/process-inline-templates`


For HTML files:


- `plugin:@angular-eslint/template/recommended`
- `plugin:@angular-eslint/template/accessibility`


The template specific rules can be seen here: https://github.com/angular-eslint/angular-eslint/tree/main/packages/eslint-plugin-template#angular-eslinteslint-plugin-template


The schematic by default includes two rules for TS files:


```json
"rules": {
"@angular-eslint/directive-selector": [
    "error",
    {
      "type": "attribute",
      "prefix": "app",
      "style": "camelCase"
    }
  ],
  "@angular-eslint/component-selector": [
    "error",
    {
      "type": "element",
      "prefix": "app",
      "style": "kebab-case"
    }
  ]
}
```


This will give a linting error when you apply a different prefix for a directive or component other than “app” (it will also check for the correct casing). It is recommended to use a prefix specific for your application (see https://angular.io/guide/styleguide#component-custom-prefix).


You are already using the recommended linting rules, which is good! Most of the time, I add some more specific rules for the project, such as:


- allow no console.logs.
- enforce no unused imports (if the developer has the organize imports on save action disabled for some reason). For this, I use the `[eslint-plugin-unused-imports](<https://www.npmjs.com/package/eslint-plugin-unused-imports>)` package.
- use triple equals.
- enforce naming conventions, such as making boolean variables sound more boolean-like by prefixing is, has, can, etc.
- …


An example `.eslintrc.json` file looks like this:


```json
{
  "root": true,
  "ignorePatterns": ["projects/**/*"],
  "plugins": ["unused-imports"],
  "overrides": [
    {
      "files": ["*.ts"],
      "extends": [
        "eslint:recommended",
        "plugin:@typescript-eslint/recommended",
        "plugin:@angular-eslint/recommended",
        "plugin:@angular-eslint/template/process-inline-templates"
      ],
      "rules": {
        "@angular-eslint/directive-selector": [
          "error",
          {
            "type": "attribute",
            "prefix": "toh",
            "style": "camelCase"
          }
        ],
        "@angular-eslint/component-selector": [
          "error",
          {
            "type": "element",
            "prefix": "toh",
            "style": "kebab-case"
          }
        ],
        "@typescript-eslint/explicit-function-return-type": "error",
    "@typescript-eslint/naming-convention": [
      "error",
          {
    "selector": "variable",
    "types": ["boolean"],
    "format": ["PascalCase"],
    "prefix": ["is", "should", "has", "can", "did", "will"]
      }
    ],
        "@angular-eslint/prefer-on-push-component-change-detection": "error",
        "@typescript-eslint/no-unused-vars": "off",
        "unused-imports/no-unused-imports": "error",
        "unused-imports/no-unused-vars": [
          "warn",
          {
            "vars": "all",
            "varsIgnorePattern": "^_",
            "args": "after-used",
            "argsIgnorePattern": "^_"
          }
        ],
        "brace-style": ["error", "1tbs"],
        "eqeqeq": "error",
        "no-else-return": "error",
        "no-trailing-spaces": "error",
        "no-var": "error",
        "object-shorthand": ["error", "properties"],
        "prefer-const": "error",
        "prefer-object-spread": "error",
        "prefer-template": "error",
        "no-console": ["error", { "allow": ["warn", "error"] }]
      }
    },
    {
      "files": ["*.html"],
      "extends": [
        "plugin:@angular-eslint/template/recommended",
        "plugin:@angular-eslint/template/accessibility"
      ],
      "rules": {}
    }
  ]
}
```


There are so many linting rules you can apply. Use the ones that work for your project and team. Caution though, because some linting has an impact on your code, so make sure you know what you’re doing! If you’re not certain about the impact of a certain rule, set the severity to “warn” instead of “error” and see how many lines it will impact by running the linter.


Also, have a look at all the Eslint plugins that already exist. They might have a set of rules that you can use for your project. Examples:


- eslint-plugin-jest: https://www.npmjs.com/package/eslint-plugin-jest. Linting rules for your Jest unit tests.
- @ngrx/eslint-plugin: https://www.npmjs.com/package/@ngrx/eslint-plugin. Linting rules when using NGRX.


Run `ng lint --fix` to run the linting for your project and potentially autofix all problems.


### Linting (S)CSS files


Linting can be applied not only to TypeScript and HTML files but also to your stylesheets. To lint stylesheets, you can use [Stylelint](https://stylelint.io/user-guide/get-started/). Most of the time, the projects I work on are a bit more complex, so they use SCSS. This is possible with Stylelint by installing an SCSS configuration:


```bash
npm install --save-dev stylelint stylelint-config-standard-scss
```


Create a `.stylelintrc.json` file in the root of the project that will contain all the rules that will apply to your stylesheet. You can extend the recommended rules but also customize them. When extending the recommended rules, chances are all your existing stylesheets will contain errors. For starters, add the rules that give errors to the list of rules and set them to `null` to ignore them for now. This will give you the chance to evaluate the importance of that rule.


An example `.stylelintrc.json` file:


```json
{
  "extends": "stylelint-config-standard-scss",
  "rules": {
    "no-empty-source": null
  }
}
```


You can take a look at the different rules here:


- Stylelint rules: https://stylelint.io/user-guide/rules
- SCSS rules (from the standard config): https://github.com/stylelint-scss/stylelint-config-standard-scss/blob/main/index.js


Next, update the script in your package.json that performs the linting, so it will also include the linting of stylesheets:


```json
"lint": "ng lint && npx stylelint \"**/*.scss\""
```


Similar to Prettier, also create a `.stylelintignore` file in the root of the project to ignore specific files. It’s ok to copy the contents of the `.gitignore` file into this one.


## 5. Husky


Add Husky hooks to prevent team members from committing or pushing faulty code.


```bash
npx husky-init && npm install
```


*Note*: If you get the error `The token '&&' is not a valid statement separator in this version.`, just run the commands separately.


This will create a pre-commit hook file from Husky. It will originally contain the command npm test. I suggest updating this with npm run lint. Running tests on a small project will be fast, but as the project grows, it will take longer. So committing code will get cumbersome. Therefore, I recommend linting the project before committing as a first check.


```
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"


npm run lint
```


Create a pre-push hook file that will run the unit tests before pushing the code to Git.


```
npx husky add .husky/pre-push "npm run test"
```


This will be done only after a few commits, so it is ok that this takes a bit longer.


## 6. Use conventional Commits


[Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) is a specification for writing standardized commit messages. This way, the changes throughout your code are documented in a more consistent and human-readable way. A Conventional Commit message looks like the following:


```text
<type>[optional scope]: <description>


[optional body]


[optional footer(s)]
``` 


Example:
```text
fix: loading spinner not working on home page
``` 


The benefits of using these types of commit messages are:
1. **Semantic Versioning (SemVer) compatibility**: Conventional Commits are designed to work well with Semantic Versioning. By following a specific format for commit messages, it becomes easier to automatically determine the version number based on the type of changes introduced (e.g., feature, fix, breaking change).
2. **Automated release notes**: since Conventional Commits provide a structured and standardized way of writing commit messages, tools can automatically generate release notes from the commit history. This makes it easier to communicate changes to users, contributors, and other stakeholders.
3. **Clearer commit history**: Conventional Commits encourage a consistent and clear commit message format. This helps in creating a more readable and understandable commit history, making it easier for team members to track changes and understand the evolution of the codebase.
4. **Enables automation**: many CI/CD (Continuous Integration/Continuous Deployment) and release management tools can take advantage of Conventional Commits to automate tasks such as versioning, changelog generation, and release workflows. This reduces the manual effort required for managing the release process. E.g., Jenkins and Azure DevOps provide such extensions and plugins.
5. **Collaboration and communication**: by using a common commit message format, team members can easily understand the nature of changes made in the codebase. This improves collaboration and communication among team members, as everyone follows a consistent style when documenting changes.
6. **Facilitates maintenance**: over time, projects can grow complex, and maintaining a clear history becomes crucial. Conventional Commits make it easier to navigate and understand the commit history, aiding in maintenance tasks such as bug tracking, troubleshooting, and identifying when specific features were introduced or bugs were fixed.
7. **Integration with tooling**: many development tools and services support Conventional Commits out of the box or through plugins. This includes version control systems, CI/CD pipelines, code review tools, and more. Utilizing Conventional Commits enhances integration with these tools.


Something important to keep in mind when working with commits is to keep them small. With small commits, it will be easier to come up with a good description for the Conventional Commit message.


### commitlint


If you agree on using Conventional Commits throughout your project, that's great! Even greater would be that they could be enforced. This can be done by using [commitlint](https://github.com/conventional-changelog/commitlint/tree/master) and more specifically with the configuration for Angular: [@commitlint/config-angular](https://www.npmjs.com/package/@commitlint/config-angular).


Commitlint checks if your commit messages meet the conventional commit format. This, in combination with Husky, will ensure that the team follows the Conventional Commits format.


Run the following in your terminal to install commitlint with the Angular configuration:


```bash
npm install --save-dev @commitlint/config-angular @commitlint/cli
```


Then create a `.commitlintrc.json` file in the root of your project which will contain the configuration for commitlint:


```JSON
{ 
  "extends": ["@commitlint/config-angular"] 
}
```


This configuration file for commitlint will by default extend from `@commitlint/config-angular`.


Now to enforce this with Husky, let's add a hook:
```bash
npx husky add .husky/commit-msg "npx --no -- commitlint --edit ${1}"
``` 


A `commit-msg` hook will be created that will process your commitlint configuration to check for a valid commit message format. Now if you want to commit with a commit message that is not valid (e.g., "my faulty commit"), the commit will fail, and the output message would read:


```text
⧗   input: my faulty commit
✖   subject may not be empty [subject-empty]
✖   type may not be empty [type-empty]


✖   found 2 problems, 0 warnings
```


With the `.commitlintrc.json` file, you could have your own rules for commit message formatting. For a full list of rules, see [here](https://commitlint.js.org/#/reference-rules). 
E.g., you want to allow the type "hotfix" next to the existing types ("build", "feat",...). You could do so by adding the rule `type-enum`:


```json
{
  "extends": ["@commitlint/config-angular"],
  "rules": {
    "type-enum": [
      2,
      "always",
      [
        "hotfix",
        "build",
        "chore",
        "ci",
        "docs",
        "feat",
        "fix",
        "perf",
        "refactor",
        "revert",
        "style",
        "test"
      ]
    ]
  }
}


```


> Note: The "2" is the level. Possible values are 0 (hint), 1 (warning), and 2 (error).


It is however recommended to follow the Conventional Commit specification as closely as possible. If you do have any exceptions, make sure the team is aware of them (even new team members that recently joined).


## 7. Keep README.md up-to-date


Keep the README.md file of the project up-to-date. Use this as a starting point for every new team member starting with the project. Even better would be to add a README.md to each folder that might require additional information and point to the README in the base README of the project. You can link to other markdown files by using the hyperlink syntax, e.g., `[a relative link](shared/README.md)`.


## TLDR;


1. Communicate with your team members.
2. Add recommended extensions, project settings and snippets for VS Code.
3. Add Prettier.
4. Add linting.
5. Add Husky hooks to enforce rules on commit or push.
6. Use Conventional Commits.
7. Keep the README.md up-to-date.


If you have any questions, feel free to contact me!