Sharing Enhance elements between projects
Martin Hicks
Posted on December 15, 2022
This post introduces a new arc plugin for Enhance projects that I've recently published, with the catchy name shared-enhance-components-plugin.
Read on for details about how to use the plugin and why I came to write it.
Background
While I continued my exploration of the Enhance framework, I started to wonder how you'd go about importing elements from a shared library or UI toolkit.
In a React project, you'd import the library into the given component and then begin using its JSX tag within the render.
However, Enhance elements are dependency free by design, for good reasons.
So instead, any element you create within /app/elements
or define within /app/elements.mjs
(* more on this later), becomes automatically available in any Enhance page.
That doesn't mean you can't use dependencies where appropriate. Let's say you have a couple of elements, one of which can be used standalone, the other you always want a particular element included, you could do the following:
//app/elements/csrf-input
export default function CsrfInput({ html, state }) {
const { attrs={}, store={} } = state
const { name = 'csrf' } = attrs
return html`
<input type="hidden" name="${name}" value="${store.csrf_token}" />
`
}
import CsrfInput from "./csrf-input.mjs";
export default function CsrfForm({ html, state }) {
const { attrs={} } = state
const { action = '', method = '' } = attrs
return html`
<form action="${action}" method="${method}">
${CsrfInput({html, state})}
<slot></slot>
</form>
`
}
You can simply call the pure function to return its "render", e.g. ${CsrfInput({html, state})}
.
And I think looking at my browser's source, it means the dependent element is just the HTML entity, rather than being expanded into a web component. Which in this case is what I wanted, all be it by accident.
Pretty cool.
But what about if you want to bring in a shared UI library maintained by a different team or 3rd party?
After a search on the Enhance Discord (which is great by the way - definitely head there for help / guidance / cool things people are building), I couldn't find anything baked in.
I did see a comment from Macdonst (one of the begin/enhance team), who was explaining how you could do all of your imports in an /app/elements.mjs
file, and then they would be available throughout your app just like a first-party element you've created in /app/elements
- nice.
He went on...
The above is begging to be a plug-in.
So I thought why not.
What is this elements.mjs file anyway?
It's not mentioned much in their docs at all. In fact, at the time of writing, it's only referenced within the Deploy to Fastify section.
However, if you look at the code for the arc-plugin-enhance
(which is the plugin that orchestrates arc to have a catch-all lambda for each /app/api/ route, among many other things), you'll see that 4 locations are checked for elements to enhance:
let pathToModule = join(basePath, 'elements.mjs')
let pathToPages = join(basePath, 'pages')
let pathToElements = join(basePath, 'elements')
let pathToHead = join(basePath, 'head.mjs')
i.e.
/app/elements.mjs
-
/app/pages
(any mjs file in here) -
/app/elements
(any mjs file in here) -
/app/head.mjs
The elements.mjs file is basically returning a keyed object mapping the tag name (e.g my-component
) to a corresponding element function (e.g. MyComponent) (which can be located anywhere, like node_modules/some-package/elements/MyComponent.mjs
)
import MyComponent from 'some-package/elements/MyComponent.mjs'
let elements = {
'my-component': MyComponent
}
export default elements
So that's a pretty powerful in-built mechanism for linking things together right there.
The need for a potential plugin is that you may npm install
a package with 10s, 100s of components you'd like to use, so having a plugin do most of the leg work is helpful.
Also, defining a map between the tag name and pure function doesn't seem to have any draw back - I'm fairly certain elements are only processed if they're referenced within a page and / or element - so mapping a lot of potentially unused components seems to have no issues
Building the plugin
Enhance (the way I'm using it), is wrapped with Architect. Meaning you can easily deploy your enhance app to AWS, or to Begin.
Because it uses Architect's sandbox to run locally, and its hydration mechanism to hydrate your Lambda function code before deployment, you can also tap into Arc's powerful plug-in system.
I'd previously had a go at an Architect plugin and got a little lost. Since then (18 months or so ago), they've massively expanded their plug-in docs and improved the available lifecycle hooks you can tap into.
I also had a good look at some of the plugins listed in their repository, which helped me figure things out.
After an afternoon or so's work I had an acceptable (just about) version of a plugin which works as follows:
- You add a component package or UI library to the .arc file under the plugin's unique @ pragma -
@shared-enhance-components-plugin
- this informs the plugin which external packages to import component elements from. - On arc hydration (which happens during sandbox start, and pre arc deploy), the plugin analyses the available functions that can be imported from the given package:
- either from a specific folder, in which case each .mjs / js file will be mapped into elements.mjs
- or from an index.js file, in which case each named export will be mapped into elements.mjs
- Tag names are inferred by de-camelcasing the function name
- Any existing elements you've manually added to elements.mjs will be preserved (auto generated lines include an eol comment)
Example usage
I think it's pretty easy to use.
- First install the plugin and tie it into your project's arc file.
npm install @hicksy/shared-enhance-components-plugin
//.arc
@app
myproj
@plugins
enhance/arc-plugin-enhance
hicksy/shared-enhance-components-plugin
nb: you have to drop the preceding @ on scoped package names in the .arc file so they don't collide with the pragma names
- Install some shared elements
For example, we could install these example Enhance form elements - https://github.com/enhance-dev/form-elements.git
npm install git+https://github.com/enhance-dev/form-elements.git
- Tell the plugin to use this package
//.arc
@app
myproj
@plugins
enhance/arc-plugin-enhance
hicksy/shared-enhance-components-plugin
@shared-enhance-components-plugin
hicksy/enhance-csrf 'elements'
enhance/form-elements
Note in the above example the package we've just installed is referenced just by it's name (dropping the @ again to avoid collision with arc pragmas) - we know by looking at this package all of it's components are named exports from a route index.js file.
For @hicksy/enhance-csrf package, we pass a second arg to the plugin, this tells the plugin that the components are individual files within the specific folder.
- Start
npm start
The plugin-hook will fire, and the /app/elements.mjs
file will be auto-populated with the tag-name's and import declarations as required.
// /app/elements.mjs
import CsrfInput from '@hicksy/enhance-csrf/elements/csrf-input.mjs' //automatically inserted by shared-enhance-components-plugin
import CsrfForm from '@hicksy/enhance-csrf/elements/csrf-form.mjs' //automatically inserted by shared-enhance-components-plugin
import { CheckBox } from '@enhance/form-elements/index.js' //automatically inserted by shared-enhance-components-plugin
import { FieldSet } from '@enhance/form-elements/index.js' //automatically inserted by shared-enhance-components-plugin
import { FormElement } from '@enhance/form-elements/index.js' //automatically inserted by shared-enhance-components-plugin
import { LinkElement } from '@enhance/form-elements/index.js' //automatically inserted by shared-enhance-components-plugin
import { PageContainer } from '@enhance/form-elements/index.js' //automatically inserted by shared-enhance-components-plugin
import { SubmitButton } from '@enhance/form-elements/index.js' //automatically inserted by shared-enhance-components-plugin
import { TextInput } from '@enhance/form-elements/index.js' //automatically inserted by shared-enhance-components-plugin
let elements = {
'csrf-input': CsrfInput, //automatically inserted by shared-enhance-components-plugin
'csrf-form': CsrfForm, //automatically inserted by shared-enhance-components-plugin
'check-box': CheckBox, //automatically inserted by shared-enhance-components-plugin
'field-set': FieldSet, //automatically inserted by shared-enhance-components-plugin
'form-element': FormElement, //automatically inserted by shared-enhance-components-plugin
'link-element': LinkElement, //automatically inserted by shared-enhance-components-plugin
'page-container': PageContainer, //automatically inserted by shared-enhance-components-plugin
'submit-button': SubmitButton, //automatically inserted by shared-enhance-components-plugin
'text-input': TextInput //automatically inserted by shared-enhance-components-plugin
}
export default elements
Publishing to NPM
I've never published a package to NPM before so this was all new to me.
Previous node packages I've created have all been private and we'd opted to npm installing from the git repo address rather than purchasing a private packages account from npm.
Publishing was really straight forward (aside from accidentally deploying it unscoped, and then deciding I'd prefer to release it scoped to my username).
I followed this guide to npm publishing - thanks Zell!
And there you go...
A small plugin that hopefully eases the use of shared Enhance elements and hopefully making it more compelling for authors to share libraries of their elements for others to use?
There's no doubt a few wrinkles will be discovered, and the plugin code could do with a tidy, but I'm pretty happy with it.
Get in touch on GitHub if there's any issues, improvements or feature ideas.
Posted on December 15, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.