-
Notifications
You must be signed in to change notification settings - Fork 2
How to build a component for UCLA Library
At UCLA Library, 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 project’s 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.
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 build. Please assign the issue to yourself.
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, please release your components 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 you’re new to this process, start by picking an easy component. Each component includes a label saying "easy", "medium" or "hard", so start with an easy one.
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.
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.
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
Node All our components are Vue components, that live in a Nuxt project. 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.
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. Please see the README.md file for more information on how linting is setup.
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 examples already in the project. 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/BlockWork
(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/BlockWork"
export default {
title: "Block / Work",
}
export const Default = () => ({ // The first story should be called Default
components: { BlockWork },
template: `<block-work/>`, // You probably will have props on here
})
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
.
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.
- Mock data and Props
Each project includes a ~/stories/mock-api.json
that is a JSON file that you can use to mock a typical API response in your Stories.
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 responsive-image
component expects. This component is included in the project, 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 responsive-image
shaped image object as a prop on it.
import BlockWork from "~/components/BlockWork"
import { data as API } from "~/stories/mock-api.json"
export default {
title: "Block / Work",
}
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"/>`,
})
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.
- The Funkhaus Best Practices guide is here: +Best Practices Guide
- Some quick tips of what we like to see: https://github.com/funkhaus/best-practices
- 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/BlockWork.vue
.
We never go more than one level deep inside ~/components
. So a component called section-about-awards
would live at ~/components/SectionAboutAwards.vue
.
Never abbreviate the component name.
Design The Component Request will include a link to the designs. These are generally made using Figma and can be viewed in your web browser.
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/BlockWork.vue
<template>
<div class="block-work">
<h2 class="title"/>
</div>
</template>
<style lang="scss" scoped>
.block-work {
// Styles here
.title {
// Styles here
}
// Hover states
@media #{$has-hover} {
&:hover {
// Styles here
}
}
// Breakpoints
@media #{$lt-tablet} {
// Styles here
}
}
</style>
Recommendations:
- Component name and component root class name are the same.
- Use kebab-case CSS class names, not PascalCase or camelCase.
- Our build process uses SCSS, so all styles should be nested under the root namespace. Never put styles outside this root namespace.
- 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
~/assets/styles/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. - 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).
- The CSS should be ordered as per the order of the template, top to bottom. It should be easy to find the CSS that relates to the markup, based on the order it appears in your code.
- Use semantic class names. Meaning: Name it what is does, not what it looks like. Read this for more explanation.
- Please no generic CSS resets.
- No margins on the root level class. The component should be self contained.
- 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. - We use
px
units mostly. The exception is unitless units forline-height
. We also usevh
andvw
if needed. - When using
100vh
you should use the CSS varvar(--unit-100vh)
instead. See here for an explanation as to why. - Avoid using element names to style. Don’t do
ul {}
, rather do this.list {}
. - If you are using
z-index
in your component, please set the root element to bez-index: 0
and increment by10
for all layers inside your component. This ensures your component wont conflict with anyz-index
used at the page level.
Fonts
Any required custom font’s will be included in the project repo. The family name will be defined as CSS vars in ~/assets/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 regular components.
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/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: Event name should always be kebab cased. See here.
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 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 static site generation in Nuxt. Remember that when the site is generated by the server, that the "device" will be different than what the user has. 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"
>
Once you’ve finished, you’re ready to make a PR to the project branch! See: "How to submit a component for review"