Skip to content

Commit

Permalink
Merge branch 'refs/heads/dev'
Browse files Browse the repository at this point in the history
  • Loading branch information
kgscialdone committed Dec 11, 2023
2 parents 3af75bd + ed98ace commit 998838b
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 70 deletions.
54 changes: 38 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Facet is a single-file web library that allows for the easy, declarative definit
## Installation
You can download `facet.min.js` from this repository and reference it locally, or retrieve it directly from a CDN like JSDelivr. Facet will automatically detect component definitions in your page's HTML and convert them into web components.
```html
<script src="https://cdn.jsdelivr.net/gh/kgscialdone/[email protected].0/facet.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/kgscialdone/[email protected].1/facet.min.js"></script>
```

## Defining Components
Expand Down Expand Up @@ -38,10 +38,6 @@ You can also define a list of attributes to be observed by the component; any at
In many cases, it's beneficial to be able to define attributes where a component is used, and have those attributes change the behavior of elements inside the component. Facet achieves this through inherited attributes, which make copying attributes deeper into your components quick and easy.
```html
<template component="labeled-input">
<style>
label, input { display: block; }
label[required]::after { content: ' *'; color: red; }
</style>
<label inherit="name>for required"><slot>Input</slot></label>
<input inherit="name>id type value required placeholder">
</template>
Expand Down Expand Up @@ -82,10 +78,10 @@ Since Facet components are defined entirely in HTML, you don't have the opportun
Event handler scripts don't have to be at the top level of the component; they'll be attached to whatever their parent element is, allowing you to easily define complex behaviors for any part of your component.
```html
<template component="click-alert">
<style>div { width: 100px; height: 100px; background: rebeccapurple; }</style>
<div> <!-- Click handler will be attached here!-->
<button> <!-- Click handler will be attached here!-->
Alert me!
<script on="click">alert("I've been clicked!")</script>
</div>
</button>
</template>
```

Expand All @@ -107,6 +103,8 @@ In addition, event handler scripts have access to a small handful of magic varia
| `root` | The host component's shadow root (or the host component if `shadow="none"`).
| `event` | The event that triggered the handler.

You can also adjust how event handler scripts act with the `once`, `capture`, and `passive` attributes, which correspond to their respective equivalents in [`addEventListener`'s `options` parameter](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#options). If the attribute is present, the corresponding option will be set to `true`, otherwise it will follow the browser defaults.

## Mixins
Facet also provides a mixin system in order to facilitate code reuse between similar components. Like components, mixins are defined with `<template>` elements, and their contents are appended to the shadow DOM of any component they're mixed into.
```html
Expand All @@ -133,25 +131,49 @@ Mixins with the `global` attribute will be automatically applied to all componen
</template>
```

Mixins with the `prepend` attribute will prepend their contents instead of appending them.
```html
<template mixin="lorem" prepend>Lorem ipsum dolor sit amet...</template>

<template component="prepended-mixins" mixin="lorem">
is a pretty weird choice of filler text, if you think about it.
</template>
```

## Configuration Options
While Facet's defaults are designed to serve the majority of use cases out of the box, it does have a small handful of configuration options, which can be adjusted via attributes on the `<script>` tag that imports the Facet library.

### Namespacing
Requires the additional `facet` attribute on all elements using Facet's magic attributes, both inside and outside of components.
Requires a prefix on the `component` and `mixin` attributes to help avoid conflicts. The prefix defaults to `facet-` if no value is supplied to the attribute.
```html
<!-- With default prefix -->
<script src="facet.min.js" namespace></script>

<template facet component="hello-world">
<template facet-component="hello-world">
<p>Hello, <slot>world</slot>!</p>
<script facet on="connect">console.log(this)</script>
<script on="connect">console.log(this)</script>
</template>

<!-- With custom prefix -->
<script src="facet.min.js" namespace="fc-"></script>

<template fc-component="hello-world">
<p>Hello, <slot>world</slot>!</p>
<script on="connect">console.log(this)</script>
</template>
```

### Disable Automatic Discovery
Prevents the automatic discovery of component and mixin definitions, requiring you to manually call `facet.discoverDeclarativeComponents` yourself.
```html
<script src="facet.min.js" libonly></script>
<script defer>facet.discoverDeclarativeComponents(document.body)</script>
<script defer>facet.discoverDeclarativeComponents(document)</script>
```

### Default Shadow Mode
Changes the default shadow root mode for new components. Equivalent to the `shadow` attribute on individual components.
```html
<script src="facet.min.js" shadow="none"></script>
```

## Javascript API
Expand All @@ -160,14 +182,14 @@ While Facet strives to never require you to write Javascript for any of its core
| Function | Description
| :------- | :--
| `facet.defineComponent(tagName, template, options)` | Define a new component.
| `facet.defineMixin(name, template, applyGlobally)` | Define a new mixin.
| `facet.defineMixin(name, template, options)` | Define a new mixin.
| `facet.discoverDeclarativeComponents(root)` | Find and define all components and mixins in a given parent element.
| `facet.createTemplateElement(content)` | Convenience method for creating `<template>` elements in Javascript.

| Variable | Description
| :------- | :--
| `facet.config.useNamespace` | If true, require the `facet` attribute on all elements using Facet magic attributes.
| `facet.config.autoDiscover` | If false, skip automatic discovery of components/mixins on page load.
| `facet.config.namespace` | The required namespace prefix for the `component` and `mixin` attributes.
| `facet.config.autoDiscover` | If false, skip automatic discovery of components/mixins on page load.
| `facet.config.defaultShadowMode` | The default shadow root mode for new components.

Facet's source code is also lovingly commented with [JSDoc](https://jsdoc.app/), which keeps it lightweight and build-step free while still enabling Typescript users to rest easy about type safety when interacting with Facet's API.

Expand Down
100 changes: 48 additions & 52 deletions facet.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// Facet v0.1.0
// https://github.com/kgscialdone/facet
// Facet v0.1.1 | https://github.com/kgscialdone/facet

/** Facet Javascript API */
const facet = {
const facet = new function() {
const mixins = {}, globalMixins = []
this.version = '0.1.1'

/**
* Define a Facet component. This is primarily for internal use; it can be called manually to define
* components in JS but the `<template component>` method should be preferred.
Expand All @@ -13,27 +15,32 @@ const facet = {
* @param {string[]} [options.observeAttrs=[]] A list of attribute names to observe (default: []).
* @param {string[]} [options.applyMixins=[]] A list of mixin names to include (default: []).
*/
defineComponent(tagName, template, { shadowMode = 'closed', observeAttrs = [], applyMixins = [] }) {
const mixins = new Set(applyMixins.concat(facet._.globalMixins).map(m=>facet._.mixins[m]).filter(x=>x))
this.defineComponent = function defineComponent(tagName, template, { shadowMode = 'closed', observeAttrs = [], applyMixins = [] }) {
const localMixins = new Set(applyMixins.concat(globalMixins).map(m=>mixins[m]))

window.customElements.define(tagName, class FacetComponent extends HTMLElement {
static observedAttributes = observeAttrs
#root = shadowMode.toLowerCase() !== 'none' ? this.attachShadow({ mode: shadowMode.toLowerCase() }) : this
#root = shadowMode !== 'none' ? this.attachShadow({ mode: shadowMode }) : this

connectedCallback() {
const content = template.content.cloneNode(true)
for(let mixin of mixins) content.append(mixin.content.cloneNode(true))
for(let mixin of localMixins) content[mixin.attachPosition](mixin.template.content.cloneNode(true))

// Attach <script on> event handlers
for(let script of content.querySelectorAll(facet._.sel('script[on]'))) {
for(let script of content.querySelectorAll('script[on]')) {
let parent = script.parentElement ?? this
let handler = new Function('host', 'root', 'event', script.innerText).bind(parent, this, this.#root)
for(let event of script.getAttribute('on').split(/\s+/g)) parent.addEventListener(event, handler)
for(let event of script.getAttribute('on').split(/\s+/g))
parent.addEventListener(event, handler, {
once: script.hasAttribute('once'),
capture: script.hasAttribute('capture'),
...(script.hasAttribute('passive') ? { passive: true } : {}) // Respect inconsistent browser defaults
})
script.remove()
}

// Mirror inherited variables and attach syncing event handlers to observed inherited variables
for(let el of content.querySelectorAll(facet._.sel('[inherit]'))) {
for(let el of content.querySelectorAll('[inherit]')) {
for(let attr of el.getAttribute('inherit').split(/\s+/g)) {
const [,ogname,rename,fn] = attr.match(/^([^\/>"'=]+)(?:>([^\/>"'=]+))?(?:\/(\w+))?$/)
const cv = this.getAttribute(ogname), filter = window[fn]
Expand All @@ -57,69 +64,58 @@ const facet = {
attributeChangedCallback(name, oldValue, newValue) { this.#event('attributeChanged', { name, oldValue, newValue }) }
#event(n, d={}) { this.dispatchEvent(new CustomEvent(n, { detail: { ...d, component: this } })) }
})
},
}

/**
* Define a mixin which can be appended after the content of other components.
* @param {string} name The name used to reference this mixin.
* @param {HTMLTemplateElement} template The `<template>` element containing the mixin's content.
* @param {boolean} applyGlobally If true, automatically applies this mixin to all components (default: false).
* @param {Object} options Mixin options
* @param {boolean} [options.applyGlobally=false] If true, automatically applies this mixin to all components (default: false).
* @param {'prepend'|'append'} [options.attachPosition='append'] Determines whether to prepend or append the mixin's content (default: 'append').
*/
defineMixin(name, template, applyGlobally=false) {
facet._.mixins[name] = template
if(applyGlobally) facet._.globalMixins.push(name)
},
this.defineMixin = function defineMixin(name, template, { applyGlobally = false, attachPosition = 'append' }) {
mixins[name] = { template, attachPosition }
if(applyGlobally) globalMixins.push(name)
}

/**
* Discover and define `<template mixin>`s and `<template component>`s.
* @param {ParentNode} root The parent element to discover inside.
*/
discoverDeclarativeComponents(root) {
for(let template of root.querySelectorAll(facet._.sel('template[mixin]')))
facet.defineMixin(template.getAttribute('mixin'), template, template.hasAttribute('global'))
this.discoverDeclarativeComponents = function discoverDeclarativeComponents(root) {
for(let template of root.querySelectorAll(`template[${facet.config.namespace}mixin]`))
this.defineMixin(template.getAttribute(`${facet.config.namespace}mixin`), template, {
applyGlobally: template.hasAttribute('global'),
attachPosition: template.hasAttribute('prepend') ? 'prepend' : 'append'
})

for(let template of root.querySelectorAll(facet._.sel('template[component]')))
facet.defineComponent(template.getAttribute('component'), template, {
shadowMode: template.getAttribute('shadow') ?? 'closed',
for(let template of root.querySelectorAll(`template[${facet.config.namespace}component]`))
this.defineComponent(template.getAttribute(`${facet.config.namespace}component`), template, {
shadowMode: template.getAttribute('shadow')?.toLowerCase() ?? facet.config.defaultShadowMode,
observeAttrs: template.getAttribute('observe')?.split(/\s+/g) ?? [],
applyMixins: template.getAttribute('mixins')?.split(/\s+/g) ?? []
})
},

/**
* Wrap an HTML string in a `<template>` element.
* @param {string} content The content.
* @returns {HTMLTemplateElement}
*/
createTemplateElement(content) {
const template = document.createElement('template')
template.innerHTML = content
return template
},
}

/** Configuration options */
config: {
/** If true, adds a check for the `facet` attribute to all selector queries.
* (default: false, declarative: true if `namespace` attribute present on importing script) */
useNamespace: !!document.currentScript?.hasAttribute?.('namespace'),
this.config = {
/** If set, prepends a namespace to the `component` and `mixin` magic attributes to reduce conflicts.
* (default: none, declarative: value of `namespace` attribute on importing script, or `facet-` if present with no value) */
namespace: document.currentScript?.hasAttribute?.('namespace')
? document.currentScript.getAttribute('namespace') || 'facet-' : '',

/** If true, automatically calls `facet.discoverDeclarativeComponents` on script load.
* (default: true, declarative: false if `libonly` attribute present on importing script) */
autoDiscover: document.currentScript && !document.currentScript.hasAttribute('libonly')
},
autoDiscover: document.currentScript && !document.currentScript.hasAttribute('libonly'),

/** Internal use only */
_: {
/** @type {{[name: string]: HTMLTemplateElement}} */
mixins: {},
/** @type {string[]} */
globalMixins: [],
/** @type {(string) => string} */
sel: s => facet.config.useNamespace ? s+'[facet]' : s
/** Default shadow root mode for declaratively defined components.
* (default: 'closed', declarative: value of `shadow` attribute on importing script) */
defaultShadowMode: document.currentScript?.getAttribute('shadow') ?? 'closed'
}

// Automatically discover Facet templates on load
;(fn => document.readyState === 'interactive' ? fn() : document.addEventListener('DOMContentLoaded', fn, {once:true}))
(() => this.config.autoDiscover && this.discoverDeclarativeComponents(document))
}

// Automatically discover Facet templates on load
if(facet.config.autoDiscover)
(fn => document.readyState === 'interactive' ? fn() : document.addEventListener('DOMContentLoaded', fn, {once:true}))
(() => facet.discoverDeclarativeComponents(document.body))
Loading

0 comments on commit 998838b

Please sign in to comment.