The Google JavaScript Style Guide is the basis for our coding style, with additional guidance here where that style guide is not aligned with ES6 or TypeScript.
Comments that explain what some block of code does are nice; they can tell you something in less time than it would take to follow through the code itself.
Comments that explain why some block of code exists at all, or does something the way it does, are invaluable. The "why" is difficult, or sometimes impossible, to track down without seeking out the original author. When collaborators are in the same room, this hurts productivity. When collaborators are in different timezones, this can be devastating to productivity.
For example, this is a not-very-useful comment:
// Set default tabindex.
if (!$attrs['tabindex']) {
$element.attr('tabindex', '-1');
}
While this is much more useful:
// Unless the user specifies so, the calendar should not be a tab stop.
// This is necessary because ngAria might add a tabindex to anything with an ng-model
// (based on whether or not the user has turned that particular feature on/off).
if (!$attrs['tabindex']) {
$element.attr('tabindex', '-1');
}
In TypeScript code, use JsDoc-style comments for descriptions (on classes, members, etc.) and
use //
style comments for everything else (explanations, background info, etc.).
In SCSS code, always use //
style comments.
In HTML code, use <!-- ... -->
comments, which will be stripped when packaging a build.
For example, rather than doing this:
<mat-button>Basic button</mat-button>
<mat-button class="mat-fab">FAB</mat-button>
<mat-button class="mat-icon-button">pony</mat-button>
do this:
<mat-button>Basic button</mat-button>
<mat-fab>FAB</mat-fab>
<mat-icon-button>pony</mat-icon-button>
Keeping modules to a single responsibility makes the code easier to test, consume, and maintain. ES6 modules offer a straightforward way to organize code into logical, granular units. Ideally, individual files are 200 - 300 lines of code.
As a rule of thumb, once a file draws near 400 lines (barring abnormally long constants / comments), start considering how to refactor into smaller pieces.
Once a feature is released, it never goes away. We should avoid adding features that don't offer high user value for price we pay both in maintenance, complexity, and payload size. When in doubt, leave it out.
This applies especially to providing two different APIs to accomplish the same thing. Always prefer sticking to a single API for accomplishing something.
All code and docs in the repo should be 100 columns or fewer. This applies to TypeScript, SCSS, HTML, bash scripts, and markdown files.
Avoid adding boolean arguments to a method in cases where that argument means "do something extra". In these cases, prefer breaking the behavior up into different functions.
// AVOID
function getTargetElement(createIfNotFound = false) {
// ...
}
// PREFER
function getExistingTargetElement() {
// ...
}
function createTargetElement() {
// ...
}
Avoid any
where possible. If you find yourself using any
, consider whether a generic may be
appropriate in your case.
For methods and properties that are part of a component's public API, all types must be explicitly specified because our documentation tooling cannot currently infer types in places where TypeScript can.
When creating a fluent or builder-pattern style API, use the this
return type for methods:
class ConfigBuilder {
withName(name: string): this {
this.config.name = name;
return this;
}
}
- Omit the
public
keyword as it is the default behavior. - Use
private
when appropriate and possible, prefixing the name with an underscore. - Use
protected
when appropriate and possible with no prefix. - Prefix library-internal properties and methods with an underscore without using the
private
keyword. This is necessary for anything that must be public (to be used by Angular), but should not be part of the user-facing API. This typically applies to symbols used in template expressions,@ViewChildren
/@ContentChildren
properties, host bindings, and@Input
/@Output
properties (when using an alias).
Additionally, the @docs-private
JsDoc annotation can be used to hide any symbol from the public
API docs.
- Only use getters and setters for
@Input
properties or when otherwise required for API compatibility. - Avoid long or complex getters and setters. If the logic of an accessor would take more than three lines, introduce a new method to contain the logic.
- A getter should immediately precede its corresponding setter.
- Decorators such as
@Input
should be applied to the getter and not the setter. - Always use a
readonly
property instead of a getter (with no setter) when possible./** YES */ readonly active: boolean; /** NO */ get active(): boolean { // Using a getter solely to make the property read-only. return this._active; }
All public APIs must have user-facing comments. These are extracted and shown in the documentation on material.angular.io.
Private and internal APIs should have JsDoc when they are not obvious. Ultimately it is the purview of the code reviewer as to what is "obvious", but the rule of thumb is that most classes, properties, and methods should have a JsDoc description.
Properties should have a concise description of what the property means:
/** The label position relative to the checkbox. Defaults to 'after' */
@Input() labelPosition: 'before' | 'after' = 'after';
Methods blocks should describe what the function does and provide a description for each parameter and the return value:
/**
* Opens a modal dialog containing the given component.
* @param component Type of the component to load into the dialog.
* @param config Dialog configuration options.
* @returns Reference to the newly-opened dialog.
*/
open<T>(component: ComponentType<T>, config?: MatDialogConfig): MatDialogRef<T> { ... }
Boolean properties and return values should use "Whether..." as opposed to "True if...":
/** Whether the button is disabled. */
disabled: boolean = false;
Avoid try-catch
blocks, instead preferring to prevent an error from being thrown in the first
place. When impossible to avoid, the try-catch
block must include a comment that explains the
specific error being caught and why it cannot be prevented.
- Prefer writing out words instead of using abbreviations.
- Prefer exact names over short names (within reason). E.g.,
labelPosition
is better thanalign
because the former much more exactly communicates what the property means. - Except for
@Input
properties, useis
andhas
prefixes for boolean properties / methods.
- Don't suffix observables with
$
.
Classes should be named based on what they're responsible for. Names should capture what the code does, not how it is used:
/** NO: */
class RadioService { }
/** YES: */
class UniqueSelectionDispatcher { }
Avoid suffixing a class with "Service", as it communicates nothing about what the class does. Try to think of the class name as a person's job title.
Classes that correspond to a directive with an mat-
prefix should also be prefixed with Mat
.
CDK classes should only have a Cdk
prefix when the class is a directive with a cdk
selector
prefix.
The name of a method should capture the action that is performed by that method rather than describing when the method will be called. For example,
/** AVOID: does not describe what the function does. */
handleClick() {
// ...
}
/** PREFER: describes the action performed by the function. */
activateRipple() {
// ...
}
- Component selectors should be lowercase and delimited by hyphens. Components should use element selectors except when the component API uses a native HTML element.
- Directive selectors should be camel cased. Exceptions may be made for directives that act like a component but would have an empty template, or when the directive is intended to match some existing attribute.
Avoid using inheritance to apply reusable behaviors to multiple components. This limits how many behaviors can be composed. Instead, TypeScript mixins can be used to compose multiple common behaviors into a single component.
Component and directive inputs for boolean and number values must use a setter to coerce values to the expected type using cdk/coercion. For example:
@Input() disabled: boolean;
get disabled(): boolean { return this._disabled; }
set disabled(v: BooleanInput) { this._disabled = coerceBooleanProperty(v); }
private _disabled = false;
The above code allows users to set disabled
similar to how it can be set on native inputs:
<component disabled></component>
Native inputs used in components should be exposed to developers through ng-content
. This allows
developers to interact directly with the input and allows us to avoid providing custom
implementations for all of the input's native behaviors.
For example:
Do:
Implementation
<ng-content></ng-content>
Usage
<your-component>
<input>
</your-component>
Don't:
Implementation
<input>
Usage
<component></component>
Prefer using the host
object in the directive configuration instead of @HostBinding
and
@HostListener
. We do this because TypeScript preserves the type information of methods with
decorators, and when one of the arguments for the method is a native Event
type, this preserved
type information can lead to runtime errors in non-browser environments (e.g., server-side
pre-rendering).
- The baseline calculation for flex elements is different than other display values, making it difficult to align flex elements with standard elements like input and button.
- Component outermost elements are never flex (block or inline-block)
- Don't use
display: flex
on elements that will contain projected content.
Always prioritize lower specificity over other factors. Most style definitions should consist of a single element or css selector plus necessary state modifiers. Avoid SCSS nesting for the sake of code organization. This will allow users to much more easily override styles.
For example, rather than doing this:
.mat-calendar {
display: block;
.mat-month {
display: inline-block;
.mat-date.mat-selected {
font-weight: bold;
}
}
}
do this:
.mat-calendar {
display: block;
}
.mat-calendar-month {
display: inline-block;
}
.mat-calendar-date.mat-selected {
font-weight: bold;
}
The end-user of a component should be the one to decide how much margin a component has around it.
This makes it easier to override styles when necessary. For example, rather than
the-host-element {
// ...
.some-child-element {
color: red;
}
}
you can write
the-host-element {
// ...
color: red;
}
The latter is equivalent for the component, but makes it easier override when necessary.
This is a low-effort task that makes a big difference for low-vision users. Example:
@include cdk-high-contrast(active, off) {
.unicorn-motocycle {
border: 1px solid #fff !important;
}
}
When it is not super obvious, include a brief description of what a class represents. For example:
// The calendar icon button used to open the calendar pane.
.mat-datepicker-button { ... }
// Floating pane that contains the calendar at the bottom of the input.
.mat-datepicker-calendar-pane { ... }
// Portion of the floating panel that sits, invisibly, on top of the input.
.mat-datepicker-input-mask { }
Targeting tag names can cause conflicts with the MDC version of the component. For this reason, use CSS class names defined by us instead of tag names. We also prefer classes over attributes for consistency.
/** Do: */
.mat-mdc-slider { ... }
/** Don't: */
mdc-slider { ... }
/** Do: */
.mat-mdc-slider-input { ... }
/** Don't: */
input[type="button"] { ... }