Enforcing front-end guidelines in an Angular project

Thomas King - Nov 14 '23 - - Dev Community

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)!

{
  // 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"
  ]
}
Enter fullscreen mode Exit fullscreen mode

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):

{
  "editor.codeActionsOnSave": {
    "source.organizeImports": true
  },
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.formatOnSave": true,
  "editor.lineNumbers": "on",
  "editor.trimAutoWhitespace": true,
  "editor.tabSize": 2
}
Enter fullscreen mode Exit fullscreen mode

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:

{
  "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"
 }
}
Enter fullscreen mode Exit fullscreen mode

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:

npm install prettier -D
Enter fullscreen mode Exit fullscreen mode

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!):

{
  "tabWidth": 2,
  "useTabs": false,
  "singleQuote": true,
  "semi": true,
  "bracketSpacing": true,
  "arrowParens": "avoid",
  "trailingComma": "es5",
  "bracketSameLine": true,
  "printWidth": 80,
  "endOfLine": "auto"
}
Enter fullscreen mode Exit fullscreen mode

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:

ng add @angular-eslint/schematics
Enter fullscreen mode Exit fullscreen mode

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:

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:

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

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:

{
  "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": {}
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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:

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. 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:

npm install --save-dev stylelint stylelint-config-standard-scss
Enter fullscreen mode Exit fullscreen mode

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:

{
  "extends": "stylelint-config-standard-scss",
  "rules": {
    "no-empty-source": null
  }
}
Enter fullscreen mode Exit fullscreen mode

You can take a look at the different rules here:

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

"lint": "ng lint && npx stylelint \"**/*.scss\""
Enter fullscreen mode Exit fullscreen mode

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.

npx husky-init && npm install
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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 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:

<type>[optional scope]: <description>


[optional body]


[optional footer(s)]
Enter fullscreen mode Exit fullscreen mode


 

Example:

fix: loading spinner not working on home page
Enter fullscreen mode Exit fullscreen mode


 

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 and more specifically with the configuration for Angular: @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:

npm install --save-dev @commitlint/config-angular @commitlint/cli
Enter fullscreen mode Exit fullscreen mode

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

{ 
  "extends": ["@commitlint/config-angular"] 
}
Enter fullscreen mode Exit fullscreen mode

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

Now to enforce this with Husky, let's add a hook:

npx husky add .husky/commit-msg "npx --no -- commitlint --edit ${1}"
Enter fullscreen mode Exit fullscreen mode


 

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:

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


✖   found 2 problems, 0 warnings
Enter fullscreen mode Exit fullscreen mode

With the .commitlintrc.json file, you could have your own rules for commit message formatting. For a full list of rules, see here
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:

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


Enter fullscreen mode Exit fullscreen mode

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!

. . . . . . . . .