Awesome
Fela plugin for Vue
Installation
yarn add vue-fela
Usage
import Vue from 'vue'
import VueFela from 'vue-fela'
import { createRenderer } from 'fela'
// 1. Create a Fela renderer
const renderer = createRenderer()
// 2. Install the plugin and pass the renderer as an option
Vue.use(VueFela, { renderer })
When the plugin is installed, a $fela
property is set on Vue's prototype
making it available on each component instance. The $fela
property provides a reference to the Fela Renderer instance that was passed as an option when the plugin was installed.
The Fela Renderer instance is also provided through the inject
interface on a fela
key. This allows you to access the renderer within functional Vue components.
Check out a full working example using Nuxt.
Rendering Rules
Since a Fela Renderer instance is assigned to Vue's prototype
, you can reference and use within a Vue component via this.$fela
.
See the Single File Component example below:
<template>
<div :class="className"/>
</template>
<script>
const rule = (props) => ({
color: props.color,
margin: 10
})
export default {
props: {
color: {
type: String,
default: 'red'
}
},
computed: {
className() {
return this.$fela.renderRule(rule, this)
}
}
}
</script>
In the example above, we are passing this
as the rule's props
to Fela's renderRule
function. Doing so will give the rule access to all the component instance properties such as color
to derive the styles.
You can of course pass any arbitrary props
object as the second argument to renderRule
.
<a name="map-rule"></a>
mapRule(rule, optProps)
To save having to define a computed prop function and return this.$fela.renderRule(rule, props)
each time, a mapRule
helper is provided:
<template>
<div :class="className"/>
</template>
<script>
import { mapRule } from 'vue-fela'
const rule = (props) => ({
color: props.color,
margin: 10
})
export default {
props: {
color: {
type: String,
default: 'red'
}
},
computed: {
className: mapRule(rule)
}
}
</script>
Passing optProps
as the second argument to mapRule
is optional.
If optProps
is omitted (like in the example above) then the Vue component instance will be passed by default. Typically this is the desired behaviour since a component's styles will generally want to be the result of its state.
In the example above, the color
prop can be configured by a parent component. This value will then be available within the component instance via this.color
. If color
is not set by the parent component, it will default to 'red' since we have set the default
field in the props
color definition.
As you might expect, all properties and methods on a Vue component instance can be used within a rule:
props
values set by parent componentsdata
values set when the component is initialisedcomputed
values derived from other properties and statemethods
that return values
Since the class names being returned from mapRule
are assigned to the component's computed
props object, any change in state referenced by a rule will trigger an update and return new class names.
<a name="map-rules"></a>
mapRules(rules, optMap, optProps)
Taking it one step further, the mapRules
helper repeats the work of mapRule
—but expects an object map of rules
rather than a single rule
function:
<template>
<div :class="container">
<h1 :class="heading">Kitten Socks</h1>
<p :class="body">The purrrfect gift for your cat.</p>
</div>
</template>
<script>
import { mapRules } from 'vue-fela'
const rules = {
// We can use object destructuring on the rule props
container: ({ highlight, textAlign }) => ({
backgroundColor: highlight ? '#DDF' : '#DDD',
padding: 16,
textAlign
}),
heading: ({ highlight }) => ({
fontWeight: highlight ? 700 : 400,
fontSize: 24
}),
body: ({ highlight }) => ({
color: highlight ? '#F00' : '#888',
fontSize: 16
})
}
export default {
props: {
highlight: {
type: Boolean,
default: false
},
textAlign: {
type: String,
default: 'left'
}
},
computed: mapRules(rules)
}
</script>
Writing all your rules in a component can start to get a little cumbersome, so you might want to consider breaking them out into a separate file:
// component-rules.js
export default {
container: ({ highlight, textAlign }) => ({
backgroundColor: highlight ? '#DDF' : '#DDD',
padding: 16,
textAlign
}),
heading: ({ highlight }) => ({
fontWeight: highlight ? 700 : 400,
fontSize: 24
}),
body: ({ highlight }) => ({
color: highlight ? '#08F' : '#888',
fontSize: 16
})
}
...and then import
them into your Vue component:
<template>
<div :class="container">
<h1 :class="heading">Kitten Socks</h1>
<p :class="body">The purrrfect gift for your cat.</p>
</div>
</template>
<script>
import { mapRules } from 'vue-fela'
import rules from './component-rules'
export default {
props: {
highlight: {
type: Boolean
},
textAlign: {
type: String,
default: 'left'
}
},
computed: mapRules(rules)
}
</script>
Calling mapRules(rules, optMap, optProps)
with no optMap
argument will result in all rules
being returned and assigned to the component's computed
props.
In the example above, this would assign container
, heading
and body
to the component's computed
props. These 3 props can then be used as class names on the 3 respective elements in the template.
This behaviour might not always be desirable, so mapRules
provides another level of control with the optMap
argument.
If you have used Vuex's mapping helpers the following interface should look familiar to you.
optMap
The optMap
argument can either be:
- An Array of Strings where:
- String values are the names of the rules to include in the object that is returned
- This allows you to filter out only the rules you require
- It does not allow you to rename the rules
- An Object of key values where:
- Keys are aliases to use as the computed prop names in the object that is returned
- Values are the names of the rules in the object map
- This allows you to both filter and rename rules
For example, using the same component and rules map from the example above:
<template>
<div :class="container">
<h1 :class="title">Kitten Socks</h1>
<p>The purrrfect gift for your cat.</p>
</div>
</template>
<script>
import { mapRules } from 'vue-fela'
import rules from './component-rules'
export default {
props: {
highlight: {
type: Boolean
},
textAlign: {
type: String,
default: 'left'
}
},
computed: {
...mapRules(rules, [
'container' // Only return the container rule
]),
...mapRules(rules, {
title: 'heading' // Only return the heading rule, but rename it to 'title'
})
}
}
</script>
In the example above you can see that we have used the object spread operator to merge multiple calls to mapRules
onto the component's computed
props object. We have also omitted the body
rule from the component-rules.js
map.
optProps
Finally, much like the mapRule
helper, optProps
can be passed as the third argument to mapRules
. Omitting optProps
will result in the component instance being passed to each of the rules as the props
argument by default.
<a name="map-styles"></a>
mapStyles(rules, optMap, optProps)
If your component has properties that clash with your rule names when mapping them to the computed
object—you have a couple of options:
- Pass an
optMap
argument tomapRules
to alias your rule names to different ones eg.iconClass: 'icon'
- Use a naming convention for all your rules that separates them from your component
props
eg.iconRule
oriconClass
The first option requires more code in your components. The second option compromises the precise naming of your rules. Neither options are ideal.
As a third option you can use the mapStyles
helper.
This helper works in exactly the same way as mapRules
, but rather than returning an object map of computed prop functions, it returns a single computed function.
The computed function returned from the mapStyles
helper will then return an object map of class names that are the result of rendering your rules
.
This allows you to assign all your rules to a single computed
property eg. styles
and render the result of each rule within your template using dot syntax eg. styles.icon
:
<template>
<div>
<svg :class="styles.icon">...</svg>
<span :class="styles.text">{icon}</span>
</div>
</template>
<script>
import { mapStyles } from 'vue-fela'
const rules = {
// Here we have a rule name that is the
// same as one of the component props
icon: ({ size, color }) => ({
width: size,
height: size,
fill: color
}),
text: ({ size, color }) => ({
fontSize: size,
color
})
}
export default {
props: {
icon: {
type: String
},
size: {
type: Number,
default: 16
},
color: {
type: 'String',
default: 'red'
}
},
computed: {
// All rules are assigned to a single computed 'styles'
// property and do not clash with any other props
styles: mapStyles(rules)
}
}
</script>
The function signature of mapStyles(rules, optMap, optProps)
is identical to mapRules(rules, optMap, optProps)
.
Read the documentation on optMap
and optProps
above.
<a name="render-rule"></a>
renderRule(renderer, rule, props)
When creating functional Vue components you can inject
the Fela Renderer instance via the fela
key:
export default {
functional: true,
inject: [ 'fela' ],
render(h, { data, children, injections }) {
// Reference to Fela Renderer instance
const renderer = injections.fela
return h('span', data, children)
}
}
To render a single Fela rule
you can either use the Fela Renderer instance's renderRule
method directly or you can use the renderRule
helper provided by Vue Fela. They do exactly the same thing—this is just a matter of style:
import { renderRule } from 'vue-fela'
const rule = ({ size }) => ({ width: size, height: size })
export default {
functional: true,
inject: [ 'fela' ],
props: { size: Number },
render(h, { props, children, injections }) {
const renderer = injections.fela
// The following 2 lines return exactly the same value
const class1 = renderRule(renderer, rule, props)
const class2 = renderer.renderRule(rule, props)
// class1 === class2
return h('span', {
class: [ class1, class2 ] // a b
}, children)
}
}
<a name="render-rules"></a>
renderRules(renderer, rules, props, optMap)
When using an object map of rules
within a functional Vue component you can use the renderRules
helper provided by Vue Fela:
import { renderRules } from 'vue-fela'
const rules = {
container: ({ fillColor }) => ({
backgroundColor: fillColor,
padding: '8px'
}),
text: ({ textColor }) => ({
color: textColor,
fontSize: '16px'
}),
}
export default {
functional: true,
inject: [ 'fela' ],
props: {
fillColor: String,
textColor: String
},
render(h, { props, children, injections }) {
const renderer = injections.fela
const styles = renderRules(renderer, rules, props)
return h('div', {
class: styles.container
}, [
h('span', {
class: styles.text
}, children)
])
}
}
You can also optionally pass optMap
as the final argument to the renderRules
helper.
This works in exactly the same way as documented above in the mapRules
helper.
Rendering Styles
To render the styles cached by the Fela Renderer we need to use fela-dom
.
However, since Fela works on both the client and the server, the method for rendering the cached styles differs between environments.
To simplify this, VueFela
handles the logic of determining what to do in each environment automatically.
Plugin Options
You can disable automatic rendering via the plugin options when installing VueFela
:
import Vue from 'vue'
import VueFela from 'vue-fela'
import { createRenderer } from 'fela'
import { render } from 'fela-dom'
const renderer = createRenderer()
Vue.use(VueFela, {
renderer, // required
autoRender: false
})
// Manually render the styles (client-side)
render(renderer)
Options that can be configured when the plugin is installed are as follows:
Option | Type | Default | Description |
---|---|---|---|
renderer | Object | Fela Renderer instance. Required | |
autoRender | Boolean | true | Enable/disable automatic rendering. When set to false the ssr and metaKeyName properties are ignored |
ssr | Boolean | true | Enable/disable SSR |
metaKeyName | String | head | Vue Meta keyName option<br>This is configured to work with Nuxt by default |
An example using Vue Meta's default values while disabling SSR can be seen below:
import Vue from 'vue'
import VueFela from 'vue-fela'
import { createRenderer } from 'fela'
Vue.use(VueFela, {
renderer: createRenderer(), // required
metaKeyName: 'metaInfo',
ssr: false
})
Universal Rendering
The easiest way to create universal Vue applications is with Nuxt. Nuxt takes care of setting up the logic for rendering Vue components on the server and rehydrating them on the client.
Nuxt uses vue-meta
to render and update tags in the <head>
of your pages. Vue Meta provides the mechanism Vue Fela needs for rendering the cached styles on the server and sending them to the client.
Because of this, the vue-fela
plugin is configured to work with vue-meta
using Nuxt's configuration options by default.
Nuxt Example
To setup Vue Fela with Nuxt you will need to create a file in the plugins
directory:
// plugins/fela.js
import Vue from 'vue'
import VueFela from 'vue-fela'
import { createRenderer } from 'fela'
// 1. Create a Fela renderer
const renderer = createRenderer()
// 2. Install the plugin and pass the renderer as an option
Vue.use(VueFela, { renderer })
Then add the plugin to Nuxt's configuration:
// nuxt.config.js
module.exports = {
plugins: [
'plugins/fela'
]
}
Check out a full working example using Nuxt.
Tests
Tests are written using jest
and vue-test-utils
.
To run the tests after cloning the repository and installing its dependencies:
yarn test
To run the tests in watch mode:
yarn test-watch
To generate a coverage report:
yarn test-coverage