The Pitch
Organising your code into discrete logic centers has compelling benefits such that it's a design pattern you see everywhere. React was my first experience with front end components, but it comes with a bunch of features which may not be appropriate for simple projects.
A vanilla component is just component functionality written within the project's codebase, and it's surprisingly easy to achieve. However, as with all things node there's a plethora of tools you can use to improve functionality and minimalise your code base, so in truth is more like "vanilla with sprinkles", but whatever.
The functionality I've tried to achieve with my components breaks down something like this:
- ES6 class instantiation and extension
- a single self contained folder for each component
- minimal boilerplate for importing / instantiating
- expose a single element ready for placement in the DOM
- auto bind class methods to browser events
- state awareness (in an abstract way, not a react way)
As in any minimalist design, it's really defined by what it doesn't do. Most notably, it doesn't attempt to determine whether it's element should be re-rendered. This is what keeps things really simple with no dogma about how state should be managed, and performant because elements are rendered when, and only when, you tell them to.
The Component
There's not much to the component itself, the code below is what I'm using presently.
/* eslint-env browser */
/* global $ */
import classNames from 'classnames'
const renderUtils = {
classNames
}
export default class Component {
constructor (initialState, layout, styles, storeKey) {
Object.assign(this, { layout, styles, storeKey })
this.hydrate(initialState)
this.el = false
}
render () {
const component = this
const locals = Object.assign(
{},
renderUtils,
this.styles,
this._state
)
const el = $(this.layout(locals))
const events = [ 'click', 'input', 'submit' ]
events.forEach((e) => {
// convert some-event to SomeEvent
const humps = e.replace(/(^\w|-\w)/g, (m) => m.slice(-1).toUpperCase())
$(`[data-on-${e}]`, el).each(function () {
const methodName = $(this).data(`on${humps}`)
$(this).on(e, component[methodName].bind(component))
})
})
// store a reference to the component on the element
el.data('component', this)
// attach to dom
if (this.el) this.el.replaceWith(el)
// store element
this.el = el
return this
}
hydrate (initialState) {
const { storeKey } = this
let state
if (!storeKey) {
this._state = initialState
} else {
state = window.localStorage.getItem(storeKey)
if (state) this._state = JSON.parse(state)
else this._state = initialState
}
return this._state
}
state (update) {
if (!update) return this._state
this._state = Object.assign(this._state, update)
const store = JSON.stringify(this._state)
if (this.storeKey) window.localStorage.setItem(this.storeKey, store)
return this._state
}
}
$.fn.extend({
component: function () {
console.log('component')
const component = $(this[0]).data('component')
if (!component) throw new Error('el doesnt have attached component')
else return component
}
})
The render
fn attaches events to the rendered element. For example if you define an element with the attribute data-on-click="clickMyButton"
, then an onClick
event will be created which calls method clickMyButton
on your component. It's not that amazing but this cuts down on loads of boilerplate.
The hydrate
and state
functions are where I've been playing around with stateful components that store their state in browser storage to survive page loads. I'm still undecided as to whether this is a good idea or not, but it's working ok for the time being.
webpack
Most of what makes this pattern feel like a component happens in webpack. Essentially, I'm using a pug and a less loader to allow you to import layout and styles into the component in js.
A simple component implementation might look like this:
import styles from './styles'
import layout from './layout'
import Component from '../Component'
import Menu from '../Menu'
export default class MyComponent extends Component {
constructor (initialState = {}) {
super(initialState, layout, styles, 'myComponent')
}
}
where ./styles
is styles.less
and ./layout
is layout.pug
.
You can see the webpack config I'm using in dotdotdown