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

<tosi-blueprint> Properties

<tosi-loader> Properties

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.