Skip to content
This repository has been archived by the owner on Jul 28, 2020. It is now read-only.

Nessie Dev Guide

Conor Cafferkey edited this page Mar 16, 2018 · 14 revisions

Nessie Dev Guide

This guide to creating Nessie components is work in progress, feel free to contribute.

Before you begin...

What is a Nessie component?

Nessie components are React components. They are:

  • ✅ Purely presentational
  • ✅ (Almost) always stateless
  • ✅ They use modular/namespaced CSS

What is a nessie component not?

Nessie components by themselves are not interactive

nessie-ui != loch-ness

nessie-ui is a library of components, while loch-ness is a webapp for viewing those components.

Dev scripts

  • yarn start: Run LochNess in dev mode.
  • yarn test: Run tests.
  • yarn tdd: Initialize test driven development.
  • yarn build: Generate up-to-date dist components and CSS for use.

Visual Tests

Post install, yarn test:v:build should be run to generate the test site.

You can then yarn test:v to run visual tests or yarn test:v -- -f <component> to test a particular component.

yarn test:ref generates new reference screenshots.

Chrome Canary is required to run visual tests.

On macOS, sometimes visual tests start failing to due Canary instances hanging around. Check for any running instances in the macOS Activity Monitor and close them, then everything should go fine.

Component source file structure

Each component lives inside a folder with that component’s name in PascalCase inside /src. Each component should have the following files:

  • ComponentName/index.jsx which renders the full component (note ComponentName initial capital)
  • ComponentName/componentName.css which contains the CSS for the component (note componentName initial lowercase)
  • ComponentName/README.md with component description in Markdown format
  • ComponentName/tests.jsx with component tests (we use Enzyme; note .jsx file extension)
  • ComponentName/driver.js with component test drivers (note .js extension)

Be sure to add your component to src/index.js when it’s ready to be exported in the Nessie dist bundle.

Component structure

Every component should:

  • ✅ have a propTypes object with all props defined (including our “standard” props: children, className, cssMap, etc.)
  • ✅ have a defaultProps object with default values defined for all props*.
  • ✅ use the buildClassName helper function to set the className of the outermost <div> (or other element) of the component
  • ✅ have defaultProps.cssMap defined by importing ./componentName.css as an object

* Example props should not be defined in defaultProps. Props to be used as example data can be added to src/defaults.json. These will be used by LochNess for demo purposes.

ComponentName/index.jsx file template

import React               from 'react';
import PropTypes           from 'prop-types';

import { buildClassName }  from '../utils';
import styles              from './componentName.css';

const ComponentName = ( {
    className,
    cssMap,
    prop1,
    prop2,
    ...
} ) =>
{
    ...

    return (
        <div
            className = { buildClassName( className, cssMap, {
                class1 : prop1,
                class2 : prop2,
                ...
            } ) }>
            ... component markup here ...
        </div>
    );
};

ComponentName.propTypes =
{
    /**
     *  CSS class name
     */
    className : PropTypes.string,
    /**
     *  CSS class map
     */
    cssMap    : PropTypes.objectOf( PropTypes.string ),
    /**
     *  Prop 1 description...
     */
    prop1     : PropTypes.prop1Type,
    /**
     *  Prop 2 description...
     */
    prop2     : PropTypes.prop2Type,
    ...
};

ComponentName.defaultProps =
{
    className : undefined,
    cssMap    : styles,
    prop1     : ...,
    prop2     : ...,
};

export default ComponentName;

Please keep the propTypes and defaultProps definitions in alphabetical order.

The <Css> injector wrapper (Deprecated – use buildClassName helper function instead)

(Almost) all Nessie components use the Css wrapper component to inject the className prop into their root element.

The Css component is sort of like a higher-order component but not quite (more like a higher-order element actually). We call this type of component an injector component.

It will always look for a .default class defined in './componentName.css' and apply this as the base CSS string.

The className of the outermost <div> (or other element) of the component must be set as the className prop of the component:

<Css cssMap = { cssMap }>
    <div className = { className }>
        ... component markup here ...
    </div>
</Css>

buildClassName helper function

The buildClassName helper function should be used to set the className of the outer div (or other element) of the component.

<div className = { buildClassName( className, cssMap, cssProps ) } ...>
    ...
</div>

cssProps object

Additional CSS classes for the component can be toggled using third argument of buildClassName: the cssProps object. The object that can contain entries of the format className: (boolean) or className: (string).

Examples
  • If no cssProps entries are defined, the outermost <div> of the component will get class="default".

  • Adding disabled: isDisabled to the CssProps object, the outermost <div> gets class="default disabled" whenever isDisabled is true.

  • Adding align: textAlign to the CssProps object, the outermost <div> gets class="default align__left" where textAlign is 'left'.

  • If we set the className argument then that value gets added to the end of the CSS string. For example, given the above examples and a className prop of 'myExtraClassName' we get: class="default disabled align__left myExtraClassName".

Applying CSS classes to elements

To apply a class .myClassName from './componentName.css' to an element inside your component (i.e. not the root element) use className = { cssMap.myClassName }:

<div className = { buildClassName( className, cssMap ) }>
    <div className = { cssMap.myClassName }>...</div>
    <div className = { cssMap.anotherClass }>...</div>
</div>

Component styling

Where possible all styling should be achieved via CSS (eg: :hover styles), rather than JS.

The CSS file should (almost) always:

  • ✅ import proto/base.css to set our common CSS properties
  • ✅ contain a .default class

It will often:

  • import proto/definitions/_colors.css or proto/definitions/_fonts.css, where necessary
  • contain the classes .disabled, .error, .fakeHovered

ComponentName/componentName.css file template

@import "../proto/base.css";
...

.default
{
    ... default CSS rules for root element ...

    .myClassName
    {
        ... default CSS rules for a child element ...
    }

    .anotherClass
    {
       ... default CSS rules for another child element ...
    }

    ...
}

.disabled
{
    ... rules for root element when component isDisabled ...

    .myClassName
    {
        ... rules for a child element when component isDisabled ...
    }

    ...
}

.error
{
    ... rules for root element when component hasError ...

    .anotherClass
    {
       ... rules for a child element when component hasError ...
    }

    ...
}

... more CSS blocks corresponding to cssProps classes ...

CSS variables

CSS variables should be used instead of magic numbers. To make the most of CSS variables they can be combined with CSS calc, e.g:

border-radius:    calc( var( --switchHeight ) / 2 );

The CSS variable naming conventions are as follows:

  • --camelCase for regular variables (no underscore or hyphen)
  • --UPPER_CASE for global constants (always underscore, never hyphen)

Component tests

Each component should implement unit tests in src/ComponentName/tests.jsx.

General guidelines

We use Enzyme to implement our component tests.

Always:

  • ✅ use describe() blocks to group tests
  • ✅ write it() blocks that read as real English sentences that start with it...
  • ✅ use Enzyme ShallowWrapper (shallow())
  • ✅ wherever possible find() nodes by matching components (find( Component ) or find( { prop: 'value' } ))
  • ✅ if it’s necessary to find a node by CSS class, use the class returned by cssMap: find( `.${cssMap.whatever}` )

Never:

  • ❌ use Enzyme ReactWrapper (mount())
  • ❌ use dive()
  • ❌ find nodes using hard-coded CSS class strings

ShallowWrapper Gotchas (and solutions)

No refs

This is a good thing. Any refs required for testing purposes should be mocked:

const wrapper = shallow(  <TextInputWithIcon /> );
const instance = wrapper.instance();

instance.refs.input = <input id="mockedInput"/>;
...

We can also test that refs are correctly set even though they are not available on the ShallowWrapper:

Let’s say we have in our component’s render method:

<input ref = { ref => this.input = ref } />

we can test that the ref gets stored under the correct key (here, this.input) by manually calling the ref callback function:

const instance  = wrapper.instance;
const inputEl   = wrapper.find( input ).node;

inputEl.ref( inputEl );

expect( instance.input ).to.equal( inputEl );

Props that are JSX nodes not included in wrapper

If you need to find() a thing contained inside a prop that is a JSX node, just call shallow() on the prop:

const wrapper = shallow( 
    <Module customHeader={ <Row><Column><Button>Click me</Button></Column></Row> } />
);
const instance = wrapper.instance();

const headerWrapper = shallow( instance.props.customHeader );

headerWrapper.find( Button ).simulate( 'click' );
...

LochNess

Displaying your component in LochNess

If you want your component to show up in LochNess, all you need to do is make sure it’s listed in src/index.js (please keep the file in alphabetical order):

...
export ComponentName          from './ComponentName';
...

If you want to add some example/demo props, you can put them in src/defaults.json. These will be used by LochNess for demo purposes.

...
"ComponentName" :
{
    "prop1" : "Example data!",
    "prop2" : ...,
    ...
},
...

(Please keep this file in alphabetical order too!)

LochNess flags

LochNess-specific flags are added for a given component using a lochness object prop:

...
"ComponentName" :
{
    ...,
    "lochness" : { ...LochNess flags... }
},
...

Marking components as beta

To be flag a given component as beta:

...
"ComponentName" :
{
    ...,
    "lochness" : { "isBeta" : true }
},
...

Disabling HTML code export

To disable the HTML code export for a given component:

...
"ComponentName" :
{
    ...,
    "lochness" : { "disableCode" : true }
},
...

This is useful for components that are wrappers for external libraries (e.g. Flounder, CodeMirror, ...)