Skip to content

Latest commit

 

History

History
434 lines (321 loc) · 14.9 KB

developer-standard.md

File metadata and controls

434 lines (321 loc) · 14.9 KB

Developer Standard

Writing Angular Components

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() {}
}

Naming Convention

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

Component Decorator

  • The selector should always be prefixed with ux- (any documentation specific components should be prefixed with uxd-), 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 for moduleId 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.

Component Module

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 as ngFor and ngIf in our component template.

Component Index

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.

Component 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 with ChangeDetectionStrategy.OnPush.
  • @Input() properties should have immutable types, such as ReadonlyArray<string> rather than string[] to help avoid changes that would not be detected when using ChangeDetectionStrategy.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.

Component Styling

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:

Never add a margin to a component element.

We should leave it up to the consuming application as to how much spacing is around any component.

// Bad
ux-checkbox {
	margin: 10px;
}

Use descriptive class names.

// Good
.nav-bar {
}

.nav-bar-logo {
}

.nav-bar-brand {
}

Use color variables in components.

// Good
.selector {
	color: @brand-primary;
}

// Bad
.selector {
	color: #abc;
}

One selector per line

// Good
.selector,
.selector-secondary {
}

// Bad
.selector, .selector-secondary {
}

Include one space before the opening brace of declaration blocks for legibility.

// Good
.selector {
}

// Bad
.selector{
}

Include one space after : for each declaration.

// Good
background-color: #ddd;
color: #fff;

// bad
background-color:#ddd;
color:#fff;

End all declarations with a semi-colon.

// Good
.selector {
	color: #2d2;
	text-align: center;
}

// Bad
.selector {
	color: #2d2;
	text-align: center
}

Comma-separated property values should include a space after each comma.

// 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;
}

Lowercase all hex values and use shorthand hex values where available.

// Good
.selector {
	color: #fff;
}

// bad
.selector {
	color: #FFFFFF;
}

Avoid specifying units for zero values.

// Good
margin: 0;

// Bad
margin: 0px;

Avoid using shorthand notation for margin and padding when only setting one or two sides.

// Good
margin-top: 10px;
margin-left: 10px;

// Bad
margin: 10px 0 0 10px;

Documenting Angular Components

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:

  1. A working example of the component. Some important options may be included in the example via the use of a Customize section.
  2. A description of the component and any associated sub-components or directives.
  3. 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.
  4. Any associated types should be described using the uxd-api-properties component.
  5. Other customization information, such as available content templates or classes that can be overridden for styling.
  6. 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 {
}

Code Snippets

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.

Plunker Support

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.

Site Navigation

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.

Automated Testing

Most component changes will require an update to the automated tests. UX Aspects uses two kinds of automated tests.

Karma 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

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.

Implementing e2e 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 () => {
        //...
    });
});

Screenshot Testing

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:

  1. npm run build:library
  2. npm run test:e2e

If there are any differences, the generated screenshots will be found under target/e2e/screenshots.