How to build a component for Funkhaus
At Funkhaus, we build our websites one component at a time. At the beginning of a project, the Project Lead defines every needed component as a GitHub Issue on the project’s repo. We call these issues “Component Requests”.

Any developer can request to build any component on any Project by requesting so via a comment on an unassigned Component Request issue in GitHub. Once your request is approved, you can build your component in the projects included Storybook, then make a PR to the project's master/main branch. Each component has a time estimate that goes with it. As a partner, you get paid once you finish the component, and only for completed components.

Below is the instructions on how to find a component to build, and how to build it to the Funkhaus standard. 

1. Request a component to build


All our projects are in GitHub as private repos. You can request access to our Funkhaus organization by contacting drew@funkhaus.us. We can also add you to our partners slack channel.

Once you’ve been given access to a project’s repo, you can browse to the GitHub issues page and you’ll see a list of “Component Requests”. Any that are unassigned, you can can request to build. Just write a comment asking for permission to build this, and the Project Lead will assign it to you. 

For example, here is a Component Request: https://github.com/funkhaus/programming-partners/issues/3

Please only pick components that you will be building that day. Don’t grab more than you can do in a single session. The idea is we don’t want a group of components to be roadblocked by a dev that is busy doing something else. Most components only take a few hours to build, so if you haven’t made progress in a day or two, we will most likely open those components up for others to build.

Some components make sense to build together, like a Work Grid and the Work Block. So be sure to grab those at the same time, or at least comment on one that you’re building the other and plan to do it next.

If youre new to this process, start by picking an easy component. Each component includes a time estimate in the title, start with a quick one. These time estimates come from the Project Lead, and just serve as a guide for how hard a component is to build, so we can estimate the time to completion of the entire project.

  • Request access to Funkhaus GitHub organization
  • Comment on a Component Request issue requesting assignment 

TODO Build a simple web page that lists all open component requests in a single place?

2. Clone the project repo into your GitHub account


The goal is to develop your component interdependently as a Stroybook story, and then open a PR to to the project repo. Your component will then go through a PR review process, before being merged into the main.

So, fork the project into your GitHub account. It is explained here for reference

  • Fork the project repo into your account (button is in top right corner of GitHub.com)

TIP: If you use git CLI, then syncing your fork to the upstream repo is a good idea. 

TIP: The GitHub desktop app is really good.

3. Install packages

Open your new forked repo in your code editor. Using the terminal, you’ll want to run this command while in the project repo directory:

npm install

  • Install npm packages

4. Setup developer environment 

Node
All our components are Vue components, that live in a Nuxt project based on our fuxt boilerplate. We develop each component using an included Nuxt-Storybook module. So first thing you need to do is install Node on your machine and configure your code editor of choice with a working Node terminal. We use Atom at Funkhaus and the Terminus package.

ENV vars
The project repo will include a .env.example file in the root. You should duplicate this file, and rename the duplicate to .env. Please ensure you keep a copy of .env.example in the repo.

This .env file contains the URL to the projects backend. Although it’s not necessarily needed for component development, it’s nice to have it working for certain components that can pull data on their own (like wp-menu components).

Vue DevTools
Vue has some powerful browser developer tools. You absolutely need these installed.

Linting and Prettier
The project has perfect linting for Vue/GQL/JS/SCSS/CSS and HTML using Prettier and ESLint. The ruleset is the Vue style guide "Recommended" spec. These packages get installed in the project, but your code editor will need Prettier installed and configured for “Format on Save”. 

Behind the scenes, the workflow is this: Ugly code > Prettier > ESLint > Nice code!

NOTE: We use 4 space indents.

In your code editor, you'll want to install a Prettier plugin (VS Code or Atom), and set the ESLint Integration" to true and "Format on Save" to true. The settings for "prettier-eslint” should have a toggle for "Run Prettier Last", you want that set to false. We need ESLint to run last, this ensures the Vue Recommended specs get priority, not Prettier's specs.

  • Duplicate and rename .env.example file
  • Install Node and a terminal into your code editor of choice 
  • Enable Prettier “format on save” in your code editor

6. Create a Storybook Story

Before you start building your component, you will need to create a Story. This is used by Storybook to develop your component in isolation. “Stories” live in ~/stories/ and you’ll find some defaults in every project we do. These serve as good examples to what stories can do.

For this step to work, you’ll also need an empty Vue component at ~/components/block/work (or whatever your component is called).

Then, you need to create a file in your ~/stories directory, named after your component name. For example, if your component is called block-work then you’d have this as a minimum, located at ~/stories/BlockWork.stories.js.

import BlockWork from "~/components/block/work"

export default {
    title: "Project name / BlockWork",
}

export const Default = () => ({ // The first story should be called Default
    components: { BlockWork },
    template: `<block-work/>`, // You probably will have props on here
})

  • Create an empty Vue SFC component file
  • Create a “Default” Storybook story file

TIP: You can repeat lines 7-10 to show multiple variations of a story. Like, what happens if a text prop is really long? Or if a image prop is empty etc. Be sure to name the second story something other than Default.

Sometimes you may need to add custom styles to a story. You can do this by importing a .scss  file in the story, although this is rarely necessary.

If the designs call for a different background color than the default (white). you can specify this on a per story basis: 

export default {
    title: "WpComponents / WpImage",
    parameters: {
        backgrounds: {
            default: "black",
        },
    },
}

This is useful because
1. You don't need to import custom styles
2. Users don't have to select the correct color to view the story
3. The background color doesn't change when you open in a new tab to use dev tools.

7. Using Storybook

Once you have a component and associated story file setup, you can run Storybook like so:

npm run storybook

This will open the storybook in a new browser window. It should always appear at http://localhost:3003/ . You can now browse all the components in the project. 

TIP: If you use the “Open canvas in new tab” button on the top right hand corner of a component view, then you’ll be able to see your component in an isolated environment and use the Vue Devtools.

8. Mock data and Props

Each project includes a ~/stories/mock-api.json that is a JSON file that you can use to mock an typical WordPress API response in your Stories. 

Read this to understand how the mock-api.json file is built: +How to use mock API data with your Story 

You should look through the file, and you’ll see it contains a bunch of common data structures. A useful one is the image that is the same shape that the wp-image component expects. This component is included in all our projects, and builds out a responsive image. 

This is how you’d use the mock API in the above block-work story example, assuming it needed a wp-image shaped image object as a prop on it.

import BlockWork from "~/components/block/work"
import { data as API } from "~/stories/mock-api.json"

export default {
    title: "Project name / BlockWork",
}

export const Default = () => ({
    components: { BlockWork },
    data() {
        return {
            API, // This loads the mock API into the template for use below
        }
    },
      template: `<block-work :image="API.image.node"/>`,
})

9. Develop your component 

Now it’s time to start building your component. Start by reading the Component Request carefully. 

A general principal we follow is, “Props in, Events out”. Meaning that your component should not access any data other than what it is given by a Prop. And it should only interact with the outside code by emitting a Vue event or a native event if needed.

The same goes with styling. Your component should not style anything other than it’s internals. So, no margins or positioning. Assume your component is going to be used in multiple places, even if it isn’t in the designs that way.

Planning
Be sure to read all these best practice guides.
  1. The Funkhaus Best Practices guide is here: +Best Practices Guide 
  1. Some quick tips of what we like to see: https://github.com/funkhaus/best-practices
  1. Essential reading. We try to stick to the “Priority C: Recommended” spec: https://vuejs.org/v2/style-guide/

Component name
The component name will be defined in the Component Request. You should use this name. 

It should be placed in the folder that matches it’s name. So, if the component is named block-work then it would be located at ~/components/block/work.vue

We never go more than one level deep inside ~/components. So a component called section-about-awards would live at ~/components/section/about-awards.vue.


Design
The Component Request will include a link to the designs. These are generally made using Adobe XD and can be viewed in your web browser. Note that the XD page has 3 different modes you can toggle between. The controls for these are in the top right hand corner of the screen. The {} mode is the most useful for coding. The first mode (the comment bubble icon) is useful for seeing the website as a prototype and browsing around the site.  

You can also export SVG and image files out of the XD page. If you select a graphic, then on the right hand {} panel, you’ll see a blue download button. Graphics need to be specifically marked as downloadable by the designers, so sometimes they get missed. You can often times find the same graphic in another part of the XD file, if not, you can request it as a comment on the Component Request issue in GitHub.

Props
What props do you need? These will be defined in the Component Request. All Props should have a default defined (or a required boolean). For example:

    props: {
        image: {
            type: Object,
            default: () => {}
        },
        items: {
            type: Array,
            default: () => []
        },
        to: {
            type: String,
            default: ""
        }
    } 

HTML structure
Please keep SEO in mind when you build this. We want to see that you understand HTML semantics. 

Don’t use h1 as that is reserved for the site name. Chances are you will need h2 or h3. Please use relevant HTML5 elements.

CSS structure
We like our CSS to be very organized, and it is a big sticking point for us. Please see the below example, and then see the notes below.

This is a Vue SFC component located at ~/components/block/work.vue

<template>
    <div class="block-work">
        <h2 class="title"/>
    </div>
</template>
<style lang="scss" scopped>
.block-work {
    // Styles here    

    .title {
        // Styles here
    }

    // Hover states
    @media #{$has-hover} {
        &:hover {
            // Styles here
        }
    }

    // Breakpoints
    @media #{$lt-tablet} {
        // Styles here    
    }
}
</style>

Recommendations:
  1. Component name and component root class name are the same.
  1. Use kebab-case CSS class names, not PascalCase or camelCase.
  1. Our build process uses SCSS, so all styles should be nested under the root namespace. Never put styles outside this root namespace.
  1. Hover states and breakpoint media queries use SCSS syntax as shown, and should be all grouped in the last part of the style tag. These vars are defined in ~/styles/scss-variables.scss if you are curious. We do it like this so we can change the breakpoint in one place as needed, and to not pollute the styles with lots of hover code.
  1. We style hovers in a media query to stop them from unexpected issues on touch devices (double tap to activate a hovered link for example).
  1. The CSS should be ordered as per the order of the template. It should be easy to find the CSS that relates to the markup, based on the order it appears in your code.
  1. Use semantic class names. Meaning: Name it what is does, not what it looks like. Read this for more explanation.
  1. Please no generic CSS resets.
  1. No margins on the root level class. The component should be self contained.
  1. Note the default colors used through the site (see the ~/styles/css-variables.scss file). So often times you won’t need to set a background-color on the component, or font color.
  1. We use px units mostly. The exception is unitless units for line-height. We also use vh and vw if needed. 
  1. When using 100vh you should use the CSS var var(--unit-100vh) instead. See here for an explanation as to why.
  1. Avoid using element names to style. Don’t do ul {}, rather do this .list {}.
  1. If you are using z-index in your component, please set the root element to be z-index: 0 and increment by 10 for all layers inside your component. This ensures your component wont conflict with any z-index used at the page level.

Fonts
Any required custom font’s will be included in the project repo. They will be defined as CSS vars in ~/styles/css-variables.scss so you can look there for the correct CSS var you need for a given font.

For example:
.sub-title {
    font-family: var(--font-secondary);
}

Images and SVGs
The project includes an SVG webpack loader. So any SVGs would be placed in the ~/assets/svg directory and imported like so:

<template>
  <div class="block-work">
      <svg-funkhaus-logo/>
  </div>
</template>
<script>
import SvgFunkhausLogo from "~/assets/svg/funkhaus-logo"
export default {
  components: {
    SvgFunkhausLogo
  }
}
</script>

CSS vars needed
Often the project will make use of repetitive CSS values. We use CSS variables to make this easier on the team.

Please look in ~/styles/css-variables.scss for a list of all the ones defined on you project. Fonts, colors and units (for consistent margins and padding etc) are the most common use cases for these.

Vue events
If your component is required to emit an event, it will be explained in the Component Request. Please respect the name provided, as it’s probably being listened to somewhere else in the project. 

TIP: Events name should always be kebab cased. See here.

10. Common mistakes - Don’t do these

This is a list of common mistakes we see. Please avoid these.

Don’t access the DOM directly
Don’t access the DOM directly. We never want to see any querySelectors! 

Use this.$refs and this.$el if you need, but always better to use a computed property where possible.

BAD:
<template>
    <div class="panel-menu" @click="onClick">
        <!-- Some code here -->
    </div>
</template>
<script>
export default {
    methods: {
        onClick() {
            element = document.querySelector('panel-menu')
            element.style.transform = 'translateX(100%)'
        }    
    }
}
</script>

GOOD:
<template>
    <div :class="classes" @click="onClick">
        <!-- Some code here -->
    </div>
</template>
<script>
export default {
    data() {
        return {
            isOpened: false        
        }
    },
    computed: {
        classes() {
            return ["panel-menu", {"is-opened": this.isOpened}]
        }
    },    
    methods: {
        onClick() {
            this.isOpened = !this.isOpened
        }    
    }
}
</script>
<style scoped>
.panel-menu {
    // States
    &.is-opened {
        transform: translateX(100%);
    }
}
</style>

Don’t use logic in your template
Avoid logic in your templates. Use computed properties or methods for this.

BAD:
<template>
    <div :class="['panel-menu', {'is-opened': isOpened}]">
        <nuxt-link 
            v-if="user.role == 'admin '? true : false" 
            v-text="Admin Dashboard"
        />
        <button @click="isOpened = false">Close menu</button>
    </div>
</template>

GOOD:
<template>
    <div :class="classes">
        <nuxt-link 
            v-if="isAdmin" 
            v-text="Admin Dashboard"
        />
        <button @click="closeMenu">Close menu</button>
    </div>
</template>
<script>
export default {
    computed: {
        classes() {
            return ['panel-menu', {'is-opened': this.isOpened}]
        },
        isAdmin() {
            return user.role == 'admin
        }    
    },
    data() {
        return {
            isOpened: true
        }
    },
    methods: {
        closeMenu() {
            this.isOpened = false
        }
    }
}
</script>

Don’t import a component and name it something other than it’s file path.
We want it to be easy to find a component in the filesystem. So use the same names throughout.

BAD:
<template>
    <div class="grid-work">
        <block v-for="item in items" :key="item.id"/>
    </div>
</template>
<script>
import Block from "~/components/block/work"
</script>


GOOD:
<template>
    <div class="grid-work">
        <block-work v-for="item in items" :key="item.id"/>
    </div>
</template>
<script>
import BlockWork from "~/components/block/work"
</script>

Don’t use extra markup to solve a style
We never want to see extra markup added to simply solve a style issue. Ideally your markup is as minimal as possible.

 BAD:
<template>
    <div class="global-logo">
        <nuxt-link to="/">
            <svg-logo-funkhaus/>
        </nuxt-link>
    </div>
</template>

 GOOD:
<template>
    <nuxt-link class="global-logo" to="/">
        <svg-logo-funkhaus/>
    </nuxt-link>
</template>
<style scoped>
.global-logo {
    display: block;
}
</style>

Don’t use v-if for showing/hiding content at certain breakpoints 
Using v-if to hide or show content at certain breakpoints causes a lot of node-mismatch errors when using server side rendering in Nuxt. So you should only use CSS breakpoints to hide/display content. 

Don’t deeply nest your CSS for no reason
Your CSS should be as least specific as possible. If you are selecting more than 2 levels, question if you are doing things right!

BAD:
<style lang="scss" scoped>
.menu-main {
  .list {
    // Some style
    .item {
      a.link {
        // Some style
      }
    }
  }
}
</style>

GOOD:
<style lang="scss" scoped>
.menu-main {
  .list {
    // Some style
  }
  .link {
    // Some style
  }
}
</style>

Don’t use string concatenation 
Use template literal instead of string concatenation.

BAD:
:href="'mailto:' + item.email"                  

GOOD:
:href="`mailto:${item.email}`"

Don’t use index’s for keys in Vue
Key’s should be unique to that component, so using an index is bad practice. It’s better to use no key than index as a key.

BAD:
<div
    v-for="(item, i) in items"
    :key="i"
>

GOOD:
<div
    v-for="(item, i) in items"
    :key="item.id"
>

11. Finished - Make a PR

Once you’ve finished, you’re ready to make a PR to the project branch!