This is a guide on how you should write Angular components.
The following syntax should be used:
import { Component } from '@angular/core';
@Component({
selector: 'ux-sample',
templateUrl: './sample.component.html'
})
export class SampleComponent {
constructor() {}
}
Files and folders should be named using lower kebab case, with each component file following the format name.component.ext
(replacing name with the component name
and ext
with the associated file extension).
An example folder structure would be:
components
-- spark
---- spark.component.ts
---- spark.component.html
---- spark.component.less
---- spark.module.ts
---- index.ts
-- flippable-card
---- flippable-card.component.ts
---- flippable-card.component.html
---- flippable-card.component.less
---- flippable-card.module.ts
---- index.ts
- The selector should always be prefixed with
ux-
(any documentation specific components should be prefixed withuxd-
), this will help avoid any potential conflicts with selectors in other libraries of a user's application. - Exclude
moduleId
property. The Angular component interface has a field formoduleId
which is used to support relative paths, primarily for SystemJS module loader to load templates and stylesheets. As part of our build process we inline templates to allow us to support the most common bundlers and module loaders so this property is not required. - Template urls should begin with a
./
to ensure they are relative paths. - Use the tag element instead of wrapping in a container element. The tag element can be styled and have events and bindings using the
:host
property in the decorator (or using a HostListener). - Define inputs and outputs in class rather than in component metadata.
Each component should have its own module file that will import everything it requires, export the component so other modules can use it and declare the component.
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SampleComponent } from './sample.component';
@NgModule({
imports: [ CommonModule ],
exports: [ SampleComponent ],
declarations: [ SampleComponent ]
})
export class SampleModule { }
Note: In the example above we import the
CommonModule
, this is required if we need to use Angular's built in directives such asngFor
andngIf
in our component template.
Each component should have an index.ts file in its folder. This should export each component or service class associated with the component to allow consumers to import any classes they need for things like dependency injection. e.g.:
export * from './spark.module';
export * from './spark.component';
It is also very important to export the component index.ts
file from the root index.ts
file e.g.:
/*
Export Modules, Components & Services
*/
export * from './components/checkbox/index';
export * from './components/ebox/index';
export * from './components/flippable-card/index';
export * from './components/progressbar/index';
export * from './components/radiobutton/index';
export * from './components/spark/index';
export * from './components/toggleswitch/index';
export * from './services/color/index';
This allows consumers to import from @ux-aspects/ux-aspects
rather than having to specify the full path of the class.
- The class name should consist of the component name followed by
Component
, written in upper camel case. - Any
@Input
and@Output
variables should be defined in the class rather than in component metadata. - All instance variables that are used within the view should be public.
- Mark any other instance variables or functions as private that you do not wish to expose outside of the component.
- All private instance variables should be prefixed with an underscore.
- New components should use
changeDetection: ChangeDetectionStrategy.OnPush
to improve efficiency. See Angular OnPush Change Detection and Component Design for more information on designing components to work withChangeDetectionStrategy.OnPush
. @Input()
properties should have immutable types, such asReadonlyArray<string>
rather thanstring[]
to help avoid changes that would not be detected when usingChangeDetectionStrategy.OnPush
.- Any component that may be used in a form e.g. checkboxes or radiobuttons, should support both
ngModel
and an alternative two way binding property to get/set the value. - Use attributes on the template to manipulate the DOM where possible rather than using TypeScript to manipulate the DOM. In the rare occasion where it is not possible, inject
Renderer2
and use it rather than directly touching the DOM. - When using key events in the View specify the key in the attribute rather than performing a condition check on the event
keyCode
e.g.(keydown.uparrow)="upKeyPress()"
. - When binding directly to a style property in the view, place the unit in the attribute rather than using string interpolation eg.
<div [style.top.px]="topValue"></div>
rather than<div [style.top]="topValue + 'px'"></div>
. - TSLint is included in our project and your code should conform to the rules it tests for. Run
npm run lint
to check. - Where possible components should support a disabled state.
- Components should provide keyboard support for accessibility purposes.
- Each component should have automated tests written for it. See Automated Testing.
Each component should have its own stylesheet. While Angular provides component encapsulation, we cannot use this and still allow theming of components. To resolve this issue and still retain style encapsulation, every rule for a component should be inside a tag selector. For example, our ux-checkbox
component stylesheet would look like this:
ux-checkbox {
// rules go in here
}
The tag element should be styled rather than adding a div
in the component template and adding a class to it.
We should follow this style guide when writing our stylesheet, below are some of the most important points:
We should leave it up to the consuming application as to how much spacing is around any component.
// Bad
ux-checkbox {
margin: 10px;
}
// Good
.nav-bar {
}
.nav-bar-logo {
}
.nav-bar-brand {
}
// Good
.selector {
color: @brand-primary;
}
// Bad
.selector {
color: #abc;
}
// Good
.selector,
.selector-secondary {
}
// Bad
.selector, .selector-secondary {
}
// Good
.selector {
}
// Bad
.selector{
}
// Good
background-color: #ddd;
color: #fff;
// bad
background-color:#ddd;
color:#fff;
// Good
.selector {
color: #2d2;
text-align: center;
}
// Bad
.selector {
color: #2d2;
text-align: center
}
// Good
.selector {
box-shadow: 0 1px 2px #ccc, inset 0 1px 0 #fff;
}
// Bad
.selector {
box-shadow: 0 1px 2px #ccc,inset 0 1px 0 #fff;
}
// Good
.selector {
color: #fff;
}
// bad
.selector {
color: #FFFFFF;
}
// Good
margin: 0;
// Bad
margin: 0px;
// Good
margin-top: 10px;
margin-left: 10px;
// Bad
margin: 10px 0 0 10px;
Every component in UX Aspects should be documented fully, including all inputs, output, public functions to be used when exported, content templates, and related components or directives. A documentation section should have the following structure:
- A working example of the component. Some important options may be included in the example via the use of a Customize section.
- A description of the component and any associated sub-components or directives.
- An API listing of the inputs, outputs, and public functions. Each of these should be implemented with the
uxd-api-properties
component, containing a table row for each item. - Any associated types should be described using the
uxd-api-properties
component. - Other customization information, such as available content templates or classes that can be overridden for styling.
- The code listing, which should come from the
snippets
directory as noted below.
Some good reference examples include:
Each documentation section should have its own module to enable code splitting. The module should import any dependencies unless provided by a parent module.
Each subsection should be a separate component, and should be decorated with the @DocumentationSectionComponent()
decorator, passing the class name as a string parameter.
Eg:
import { Component } from '@angular/core';
import { DocumentationSectionComponent } from '../../../../../decorators/documentation-section-component';
@Component({
selector: 'uxd-components-sorting',
templateUrl: './sorting.component.html'
})
export class ComponentsSortingComponent {
}
Any code snippets should be placed in a snippets folder in the appropriate section directory. These can be imported by the section and displayed in a uxd-snippet
component using the content attribute. All snippets are available in a snippets
object on the class.
Where possible, a Plunker example should be provided. The code snippets displayed in the section should also be used to produce the example where possible.
To add a Plunker example to a section the class should implement the IPlaygroundProvider
interface. This requires having a public playground
property on the class. The following options can be provided:
export interface IPlayground {
framework?: 'angular' | 'css';
files: {
[key: string]: string;
};
modules?: {
imports?: string | string[];
providers?: string | string[];
library?: string;
importAs?: boolean;
declaration?: boolean;
forRoot?: boolean;
}[];
}
This will automatically add an 'Edit in Plunker' link to the section header.
The site navigation is driven from json
files found in the data
folder. Each section should follow this interface:
export interface ISection {
id: string;
title: string;
component: string;
version: 'AngularJS' | 'Angular';
hybrid?: boolean;
deprecated?: boolean;
deprecatedFor?: string;
externalUrl?: string;
schematic?: string;
usage: [{
title: string;
content: string;
}];
}
This is required for your component to be displayed in the documentation site correctly.
Most component changes will require an update to the automated tests. UX Aspects uses two kinds of automated tests.
Unit tests are implemented using Karma with the Jasmine framework. An example unit test case might be described as:
it('should show the widget when [showWidget]="true"', ...);
Karma tests are implemented in a file named my-component.component.spec.ts
, and will automatically be picked up by the the Karma runner when running npm run test:karma
.
End-to-end tests should focus on use cases or requirements. When fixing a bug, the repro case for the bug will often make a good end-to-end test. For lower level unit tests, use Karma tests. An example end-to-end test case might be:
it('should allow all text to be localized', ...);
These tests can also use screenshot comparison to verify styling and colors.
For end-to-end testing, there is a separate Angular application which hosts a number of pages containing the test cases. Protractor is then used to run the tests against this live application. Both the application and the test cases live in the e2e
top level directory.
Note: Many existing e2e tests within the project currently work as unit tests. These were written before the Karma suite was added. Please stick to the above guidelines for unit tests vs. end-to-end tests when creating new tests.
To implement a new test page, create a new component under e2e/pages/app
which defines the UI of the test case. Update routes
in e2e/pages/app/app.module.ts
to link the test page into the app. Run the command npm run start:e2e
to start up the e2e application, and visit http://localhost:4000/#/my-test-case
to verify the functionality of the test UI.
To implement the tests for the new test page, create a directory under e2e/tests/components
. Create my-test-case.po.spec.ts
, which contains the page object that the tests will use. By convention, this includes a getPage
function which loads the appropriate page from the e2e application.
async getPage(): Promise<void> {
await browser.get('#/my-test-case');
}
Next, create my-test-case.e2e-spec.ts
, which will contain the Jasmine specs used to run the test cases. Set up the page object using beforeEach
.
describe('My test case', () => {
let page: MyTestCasePage;
beforeEach(async () => {
page = new MyTestCasePage();
await page.getPage();
});
it('should allow all text to be localized', async () => {
//...
});
});
To prevent style regressions we can add screenshot comparisons to e2e tests:
expect(await imageCompare('checkbox-initial')).toEqual(0);
If this test is run and there is no baseline image to compare against one will be generated in the e2e/screenshots
folder. Subsequent runs
will then test against the previous baseline image.
If a component has been updated and has visually changed, the baseline image needs to be updated otherwise tests will fail.
Follow these steps to run the tests locally:
npm run build:library
npm run test:e2e
If there are any differences, the generated screenshots will be found under target/e2e/screenshots
.