JSPM

  • Created
  • Published
  • Downloads 143
  • Score
    100M100P100Q93737F
  • License SEE LICENSE IN LICENSE

Extracts, Renders And Exports For Dynamic Render JSX Components From Within HTML.

Package Exports

  • competent

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (competent) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

competent

npm version

competent Extracts, Renders And Exports For Dynamic Render JSX Components From Within HTML.

yarn add competent

Table Of Contents

API

The package is available by importing its default and named functions:

import competent, { makeComponentsScript, writeAssets } from 'competent'

competent(
  components: !Object<string, !Function|function(new: preact.Component)>,
  config=: !Config,
): !_restream.Rule

Creates a rule for Replaceable from the restream package that replaces HTML with rendered JSX components. The configuration object will be needed to export components, so that they can then be rendered on the page using JavaScript.

  • components* !Object<string, (!Function | function(new: preact.Component))>: Components to extract from HTML and render using Preact's server-side rendering. Can be either a functional stateless component, or a Preact component constructor.
  • config !Config (optional): Options for the program. All functions will be called with the Replaceable instance as their this context.
Example Usage
<html lang="en">

<npm-package style="background:red;">splendid</npm-package>
<npm-package style="background:green;">@a-la/jsx</npm-package>
<npm-package style="background:grey;">unknown-package</npm-package>

<hello-world from="Art Deco">
  An example usage of competent.
</hello-world>
<friends count="10"/>
</html>
For example, the above HTML page can be rendered with Competent by creating a Replaceable rule:
import competent from 'competent'
import aqt from '@rqt/aqt'
import read from '@wrote/read'
import { Replaceable } from 'restream'

/**
 * A standard JSX component.
 */
const HelloWorld = ({ from, children, competent: c }) => {
  c.setPretty(false)
  return (<p>Hello World From {from}.{children}</p>)
}

/**
 * A string component.
 */
const FriendCount = ({ count }) => {
  return `You have ${count} friends.`
}

/**
 * An async component.
 */
const NpmPackage = async ({ style, children, competent: c }) => {
  c.export()
  let [pck] = children
  pck = encodeURIComponent(pck)
  const { statusCode, body } =
    await aqt('https://registry.npmjs.com/' + pck)
  if (statusCode == 404) throw new Error(`Package ${pck} not found.`)
  const { name, versions, description } = body
  const keys = Object.keys(versions)
  const version = keys[keys.length - 1]
  return <div style={style}>
    <span className="name">{name}</span>
    <span className="ver">{version}</span>
    <p>{description}</p>
  </div>
}

const CompetentExample = async () => {
  let i = 0
  const exported = []
  const file = await read('example/index.html')
  const rule = competent({
    'hello-world': HelloWorld,
    'npm-package': NpmPackage,
    'friends': FriendCount,
  }, {
    getId() {
      i++
      return `c${i}`
    },
    getProps(props, meta) {
      meta.setPretty(true, 60)
      return { ...props, competent: meta }
    },
    onFail(key, err) {
      console.error('Component %s did not render:', key)
      console.error(err.message)
    },
    markExported(key, id, props, children) {
      exported.push({ key, id, props, children })
    },
  })
  const r = new Replaceable(rule)
  const res = await Replaceable.replace(r, file)
  return { res, exported }
}

export default CompetentExample
The output will contain rendered JSX.
<html lang="en">

<div style="background:red;" id="c1">
  <span class="name">splendid</span>
  <span class="ver">1.8.0</span>
  <p>Static Web Site Generator With JSX As HTML.</p>
</div>
<div style="background:green;" id="c2">
  <span class="name">@a-la/jsx</span>
  <span class="ver">1.6.0</span>
  <p>The JSX Transform For ÀLaMode And Other Packages.</p>
</div>
<npm-package style="background:grey;">unknown-package</npm-package>

<p>Hello World From Art Deco.
  An example usage of competent.
</p>
You have 10 friends.
</html>
The logging will be output to stderr.
Component npm-package did not render:
Package unknown-package not found.
Exported packages:
[ { key: 'npm-package',
    id: 'c1',
    props: { style: 'background:red;' },
    children: [ 'splendid' ] },
  { key: 'npm-package',
    id: 'c2',
    props: { style: 'background:green;' },
    children: [ '@a-la/jsx' ] } ]

Config: Options for the program. All functions will be called with the Replaceable instance as their this context.

Name Type & Description Default
removeOnError boolean false
If there was an error when rendering the component, controls whether the HTML should be be left on the page.
getId () => string
The function which returns an id for the html element.
getProps (props: !Props, meta: !Meta, componentName: string) => Object
The function which takes the parsed properties from HTML and competent's meta methods, and returns the properties object to be passed to the component. By default, returns the properties simply merged with meta.
props* !Props: Properties.
meta* !Meta: Meta properties.
componentName* string: The name of the component.
markExported (key: string, id: string, props: !Props, children: !Array<string>) => ?
If the component called the export meta method, this function will be called at the end of the replacement rule with its key, root id, properties and children as strings.
key* string: Component key.
id* string: The ID assigned manually either via the element's id attribute, or with the getId function automatically.
props* !Props: Component properties.
children* !Array<string>: Component children.
onSuccess (componentName: string, htmlProps: !Object<string, string>) => void
The callback at the end of a successful replacement with the component's key.
componentName* string: The element name, e.g., my-element.
htmlProps* !Object<string, string>: The properties with which the component was initialised.
onFail (componentName: string, error: !Error, position: number, input: string) => void
The callback at the end of failed replacement with the component's key, error object, position number and the string which was fed to the rule.
componentName* string: The element name, e.g., my-element.
error* !Error: The error.
position* number: The position in the input text where element started.
input* string: The input string.
getContext (childContext?: !Object) => !Object
The function to be called to get the properties to set on the child Replaceable started to recursively replace inner HTML. This is needed if the root Replaceable was assigned some properties that are referenced in components.
childContext !Object (optional): The child context set by meta.setChildContext with undefined if not set.
getReplacements (componentName: string, recursiveRenderAgain: boolean) => !Array<!_restream.Rule>
The function which should return the list of replacements for renderAgain method. By default, the initial rule generated by Competent is used. The first argument passed is the key, and the second argument is the value passed via the renderAgain, that is if the component might render recursively.
componentName* string: Component key.
recursiveRenderAgain* boolean: The value passed to renderAgain.

Object<string, *> Props: The properties extracted from HTML and to be passed to the component for rendering.

Meta: Service methods for competent.

Name Type & Description
export* (shouldExport?: boolean) => void
When called, marks the component for export and adds an id if the root element of the hyper result did not have it. Individual instances can pass the false value if they don't want to get exported.
shouldExport boolean (optional): Whether to export the component.
skipRender* () => void
If this method is called, Competent will return the original match without rendering the component into static HTML. This should be used together with export to provide run-time dynamic browser rendering, without static HTML code generation.
setPretty* (isPretty: boolean, lineLength?: number) => void
The function which controls whether to enable pretty printing, and the line width.
isPretty* boolean: Whether to pretty print.
lineLength number (optional): Number of characters after which to wrap lines.
removeLine* (shouldRemove?: boolean) => void
If the component rendered a falsy value (e.g., null, ''), and the removeLine was called, Competent will remove \n___<component>. By default, this is switched off.
shouldRemove boolean (optional): Sets whether the new line should be removed (default true).
renderAgain* (doRender?: boolean, recursiveRender?: boolean) => void
After rendering the component itself, the children by default are also rendered by spawning another Replaceable stream. This is needed when a component might contain other components when rendered.
  • When recursiveRender is set to false (default), the component key will be excluded from the rule to prevent recursion.
  • No recursion is allowed otherwise the program will get stuck, unless <img/> renders <img> (no /) for example.
  • If getReplacements was used to specify how to acquire the replacements for the new child Replaceable stream, the recursiveRender arg will be pased to it.
  • doRender boolean (optional): Whether to render component again to update its inner HTML. Default true.
    recursiveRender boolean (optional): Whether to render element with the same name. Default false.
    setChildContext* (context: !Object) => void
    JSX nodes are rendered breadth-first, meaning that siblings will receive the same this context. If one of them modifies it, the another one will also pass the updated one to children, which is not always desirable. To create a fork context unique for children of sibling nodes, the child context can be set. It will be passed as an argument to getContext.
    context* !Object: The context specific for children of the node that calls renderAgain.

    DEBUG=competent

    When the DEBUG env variable is set to competent, the program will print some debug information, e.g.,

    2019-09-14T19:33:51.605Z competent render npm-package
    2019-09-14T19:33:51.648Z competent render npm-package
    2019-09-14T19:33:51.653Z competent render npm-package
    2019-09-14T19:33:51.655Z competent render hello-world
    2019-09-14T19:33:51.659Z competent render friends

    makeComponentsScript(
      components: !Array<!ExportedComponent>,
      options=: MakeCompsConfig,
    ): string

    Based on the exported components that were detected using the rule, generates a script for the web browser to dynamically render them with Preact.

    • components* !Array<!ExportedComponent>: All components that were made exportable by the rule.
    • options MakeCompsConfig (optional): The options for the make components script.

    MakeCompsConfig: The options for make components script.

    Name Type & Description
    map !Object<string, !Array<?string>>

    The map with locations from where components should be imported, e.g.,

    {
      '../components/named.jsx': [null, 'named-component'],
      '../components/default.jsx': ['default-component'],
    }

    The default export must come first in the array.

    io (boolean | !IOOptions)
    Whether to use an IntersectionObserver to render elements. If an object is given, it will be passed to the IO constructor, otherwise the default options are used (rootMargin: '76px').
    props !Object<string, *>
    Shared properties made available for each component in addition to its own properties.
    includeH boolean
    Include import { h } from 'preact' on top of the file.
    externalAssets boolean
    Whether the library functions should be required from a separate file, ./__competent-lib. Works together with writeAssets and is useful when generating more than one script.

    IOOptions extends IntersectionObserverInit: Options for the observer.

    Name Type & Description
    log boolean
    Whether to print a message to console when a component is rendered.

    ExportedComponent: An exported component.

    Name Type Description
    key* string The name of the component as passed to Competent.
    id* string The ID where the component should render.
    props* !Object Properties of the component.
    children* !Array<string> Children as strings.
    import CompetentExample from './'
    import { makeComponentsScript } from 'competent'
    
    (async () => {
      const { exported } = await CompetentExample()
      console.log(makeComponentsScript(exported, {
        map: {
          '../components/npm': ['npm-package'],
          // default first then named
          '../components': ['hello-world', 'friends'],
        },
      }))
    })()
    import { render } from 'preact'
    import NpmPackage from '../components/npm'
    
    const __components = {
      'npm-package': NpmPackage,
    }
    
    function init(id, key) {
      const el = document.getElementById(id)
      if (!el) {
        console.warn('Parent element for component %s with id %s not found', key, id)
        return {}
      }
      const parent = el.parentElement
      if (!parent) {
        console.warn('Parent of element for component %s with id %s not found', key, id)
        return {}
      }
      return { parent, el  }
    }
    
    /** @type {!Array<!preact.PreactProps>} */
    const meta = [{
      key: 'npm-package',
      id: 'c1',
      props: {
        style: 'background:red;',
      },
      children: ["splendid"],
    },
    {
      key: 'npm-package',
      id: 'c2',
      props: {
        style: 'background:green;',
      },
      children: ["@a-la/jsx"],
    }]
    meta.forEach(({ key, id, props = {}, children = [] }) => {
      const { parent, el } = init(id, key)
      const Comp = __components[key]
    
      render(h(Comp, props, children), parent, el)
    })

    Assets

    By default, the lib functions will be embedded into the source code. To place them in separate files for reuse across multiple generated scripts, the externalAssets option is used together with writeAssets method.

    Intersection Observer

    Competent can generate code that will utilise the IntesectionObserver browser capability to detect when the element into which the components needs to be rendered comes into view, and only mount it at that point. This will only work when IntesectionObserver is present either natively, or via a polyfill. When the io argument value is passed as an object rather than boolean, it will be serialised, e.g., { rootMargin: '0 0 76px 0' }.

    import CompetentExample from './'
    import { makeComponentsScript } from 'competent'
    
    (async () => {
      const { exported } = await CompetentExample()
      console.log(
        makeComponentsScript(exported, {
          map: {
            '../components/npm': ['npm-package'],
            '../components': ['hello-world', 'friends'],
          },
          io: { threshold: 10, rootMargin: '50px' },
          externalAssets: true,
        })
      )
    })()
    import { render } from 'preact'
    import { makeIo, init } from './__competent-lib'
    import NpmPackage from '../components/npm'
    
    const __components = {
      'npm-package': NpmPackage,
    }
    
    const io = makeIo({ threshold: 10, rootMargin: "50px" })
    
    /** @type {!Array<!preact.PreactProps>} */
    const meta = [{
      key: 'npm-package',
      id: 'c1',
      props: {
        style: 'background:red;',
      },
      children: ["splendid"],
    },
    {
      key: 'npm-package',
      id: 'c2',
      props: {
        style: 'background:green;',
      },
      children: ["@a-la/jsx"],
    }]
    meta.forEach(({ key, id, props = {}, children = [] }) => {
      const { parent, el } = init(id, key)
      const Comp = __components[key]
    
      el.render = () => {
        render(h(Comp, props, children), parent, el)
      }
      el.render.meta = { key, id }
      io.observe(el)
    })

    async writeAssets(
      path: string,
    ): void

    • path* string: The folder where to create the __competent-lib.js file, when the externalAssets option is passed to makeComps.
    import { writeAssets } from 'competent'
    
    (async () => {
      await writeAssets('example')
    })()
    export function init(id, key) {
      const el = document.getElementById(id)
      if (!el) {
        console.warn('Parent element for component %s with id %s not found', key, id)
        return {}
      }
      const parent = el.parentElement
      if (!parent) {
        console.warn('Parent of element for component %s with id %s not found', key, id)
        return {}
      }
      return { parent, el  }
    }
    
    export function makeIo(options = {}) {
      const { rootMargin = '76px', log = true, ...rest } = options
      const io = new IntersectionObserver((entries) => {
        entries.forEach(({ target, isIntersecting }) => {
          if (isIntersecting) {
            if (target.render) {
              if (log) console.warn('Rendering component %s into the element %s ',
                target.render.meta.key, target.render.meta.id)
              target.render()
              io.unobserve(target)
            }
          }
        })
      }, { rootMargin, ...rest })
      return io
    }

    Known Limitations

    Currently, it is not possible to match nested components.

    <Component>
      <Component example />
      <Component test boolean></Component>
    </Component>
    <component-processed />
    </component>

    This is because the RegExp is not capable of doing that sort of thing, because it cannot balance matches, however when Competent switches to a non-regexp parser it will become possible.

    Who Uses Competent

    Competent is used by:

    • Documentary: a documentation pre-processor that supports JSX for reusable components when generating README files.
    • Splendid: a static website generator that allows to write JSX components in HTML, and bundles JS compiler with Google Closure Compiler to also dynamically render them on the page.

    Dual licensed under Affero GPL and a commercial license.
    
    - Within the UK: no commercial use is allowed until the
      organisation signs up. After: see below. Sign up at:
      https://www.technation.sucks/license/.
    - Across the globe: Affero GPL. No companies affiliated
      with Tech Nation in any way (e.g., participation in
      their programs, being part of their network, hiring
      their directors), are allowed to use the software
      unless they sign up.
    
    (c) 2019 Art Deco Code
    
    The COPYING file contains the full text of the public license.
    Art Deco © Art Deco 2019 Tech Nation Visa Tech Nation Visa Sucks