blueprints
One issue with standard web-components built with tosijs is that building them
"sucks in" the version of tosijs you're working with. This isn't a huge problem
with monolithic code-bases, but it does prevent components from being loaded
"on-the-fly" from CDNs and composed on the spot and it does make it hard to
"tree shake" component libraries.
import { elements, tosiLoader, tosiBlueprint } from 'tosijs'
preview.append(
tosiLoader(
tosiBlueprint({
tag: 'swiss-clock',
src: 'https://tonioloewald.github.io/xin-clock/dist/blueprint.js?1234',
blueprintLoaded({creator}) {
preview.append(creator())
}
}),
)
)
Another issue is name-collision. What if two people create a <tab-selector> component
and you want to use both of them? Or you want to switch to a new and better one but
don't want to do it everywhere all at once?
With blueprints, the consumer of the component chooses the tag, reducing the
chance of name-collision. (You can consume the same blueprint multiple times,
giving each one its own tag.)
To address these issues, tosijs provides a <tosi-loader> loader component and
a function makeComponent that can define a component given a blueprint
function.
<tosi-loader>βthe blueprint loader
<tosi-loader> is a simple custom-element provided by tosijs for the dynamic loading
of component blueprints. It will load its <tosi-blueprint>s in parallel.
<tosi-loader>
<tosi-blueprint tag="swiss-clock" src="https://loewald.com/lib/swiss-clock"></tosi-blueprint>
</tosi-loader>
<swiss-clock>
<code style="color: var(--brand-color)">tosijs</code> rules!
</swiss-clock>
The legacy names
<xin-blueprint>and<xin-loader>still work but emit a one-time deprecation warning. New code should use<tosi-blueprint>and<tosi-loader>.
<tosi-blueprint> Attributes
srcis the url of theblueprintjavascript module (required)tagis the tagName you wish to use. This defaults to the name of the source file if suitable.propertyallows you to load a named exported property from a blueprint module (allowing one blueprint to export multiple blueprints). By default, it'sdefault.loadedis theXinPackagedComponentafter loading
<tosi-blueprint> Properties
blueprintLoaded(package: XinPackagedComponent)<tosi-blueprint>when its blueprint is loaded.
<tosi-loader> Properties
allLoaded()is called when all the blueprints have loaded.
makeComponent(tag: string, blueprint: XinBlueprint): Promise<XinPackagedCompoent>
makeComponent takes a tag of your choice and a blueprint and generates
the custom-element's class and elementCreator as its type and creator
properties.
So, instead of:
import {myThing} from './path/to/my-thing'
document.body.append(myThing())
You could write:
import { makeComponent } from 'tosijs'
import myThingBlueprint from './path/to/my-thing-blueprint'
makeComponent('different-tag', myThingBlueprint).then((packaged) => {
document.body.append(packaged.creator())
})
This is a more complex example that loads two components and only generates the test component once everything is ready:
import { tosiLoader, tosiBlueprint } from 'tosijs'
let clockType = null
preview.append(
tosiLoader(
{
allLoaded() {
const xinTest = this.querySelector('[tag="xin-test"]').loaded.creator
preview.append(
xinTest({
description: `${clockType.tagName} registered`,
test() {
return (
preview.querySelector(clockType.tagName) && preview.querySelector(clockType.tagName).constructor !==
HTMLElement
)
},
})
)
},
},
tosiBlueprint({
tag: 'swiss-clock',
src: 'https://tonioloewald.github.io/xin-clock/dist/blueprint.js?1234',
blueprintLoaded({type, creator}) {
clockType = type
preview.append(creator())
},
}),
tosiBlueprint({
tag: 'xin-test',
src: 'https://tonioloewald.github.io/xin-test/dist/blueprint.js',
})
)
)
XinBlueprint
export interface XinFactory {
Color: typeof Color
Component: typeof Component
elements: typeof elements
svgElements: typeof svgElements
mathML: typeof mathML
vars: typeof vars
varDefault: typeof varDefault
xin: typeof xin
boxed: typeof boxed
xinProxy: typeof xinProxy
boxedProxy: typeof boxedProxy // deprecated
tosi: typeof tosi
makeComponent: typeof makeComponent
bind: typeof bind
on: typeof on
version: string
}
export interface XinPackagedComponent {
type: typeof Component
creator: ElementCreator
}
export type XinBlueprint = (
tag: string,
module: XinFactory
) => XinPackagedComponent
XinBlueprint lets you provide a component "blueprint", in the form of a function,
that can be loaded and turned into an actual component. The beauty of this is that
unlike an actual component, the blueprint has no special dependencies.
So instead of defining a component like this:
import { Component, elements, vars, varDefault } from 'tosijs'
const { h2, slot } = elements
export class MyThing extends Component {
static preferredTagName = 'my-thing'
static shadowStyleSpec = {
':host': {
color: varDefault.textColor('#222'),
background: vars.bgColor,
},
}
static lightStyleSpec = {
_bgColor: '#f00'
}
content = () => [
h2('my thing'),
slot()
]
}
export const myThing = MyThing.elementCreator()
You can define a "blueprint" like this:
import { XinBlueprint } from 'tosijs'
const blueprint: XinBlueprint = (
tag,
{ Component, elements, vars, varDefault }
) => {
const {h2, slot} = elements
class MyThing extends Component {
static shadowStyleSpec = {
':host': {
color: varDefault.textColor('#222'),
background: vars.bgColor,
},
}
content = () => [
h2('my thing'),
slot()
]
}
return {
type: MyThing,
lightStyleSpec: {
_bgColor: '#f00'
}
}
}
The blueprint function can be async, so you can use async import inside it to pull in dependencies.
Note that in this example the blueprint is a pure function (i.e. it has no side-effects). If this blueprint is consumed twice, each will be completely independent. A non-pure blueprint could be implemented such that the different versions of the blueprint share information. E.g. you could maintain a list of all the instances of any version of the blueprint.