Skip to content

A wrapper over React's renderToStaticMarkup for building logicful HTML templates, using JSX, transform hooks, and a hint of magic.

License

Notifications You must be signed in to change notification settings

aaimio/logicful-templates

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

37 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Logicful templates

codecov build

A wrapper over React's renderToStaticMarkup for building logicful HTML templates, using JSX, transform hooks, and a hint of magic. πŸ§™

Show example
const Template = () => {
  const employees = [
    { name: 'Petra', age: 33, title: 'Chief Template Creator' },
    { name: 'John', age: 31, title: 'Template Hacker' },
    { name: 'Jacky', age: 26, title: 'Senior Template Engineer' },
    { name: 'Boris', age: 28, title: 'Template Acquisition Expert' },
  ];

  return (
    <html lang='en'>
      <head>
        <title>An example JSX template</title>
        <meta charSet='UTF-8' />
        <script type='text/javascript' dangerouslySetInnerHTML={{ __html: 'alert("An in-your-face message!")' }} />
      </head>
      <body>
        <div className='employees'>
          {employees.map((employee) => (
            <div key={employee.name} className='employee'>
              <div className='name'>
                {employee.name}, {employee.age}
              </div>
              <div className='title'>{employee.title}</div>
            </div>
          ))}
        </div>
      </body>
    </html>
  );
};

would compile into

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>An example JSX template</title>
    <meta charset="UTF-8" />
    <script type="text/javascript">
      alert("An in-your-face message!");
    </script>
  </head>
  <body>
    <div class="employees">
      <div class="employee">
        <div class="name">Petra, 33</div>
        <div class="title">Chief Template Creator</div>
      </div>
      <div class="employee">
        <div class="name">John, 31</div>
        <div class="title">Template Hacker</div>
      </div>
      <div class="employee">
        <div class="name">Jacky, 26</div>
        <div class="title">Senior Template Engineer</div>
      </div>
      <div class="employee">
        <div class="name">Boris, 28</div>
        <div class="title">Template Acquisition Expert</div>
      </div>
    </div>
  </body>
</html>

Usage

Setup

Install the library using:

npm i logicful-templates

TypeScript configuration

If you're using TypeScript, ensure you're using a version larger than 4.1 as we'll be using the jsxImportSource configuration to load the react JSX runtime.

Add the configuration below to your tsconfig.json:

{
  "compilerOptions": {
    "target": "ES6",
    "jsx": "react-jsx",
    "jsxImportSource": "react",
    "module": "commonjs",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "strict": true
  },
}

JavaScript + Babel configuration

If you're using plain JavaScript we'll need Babel to transpile the JSX syntax into regular JS.

Add the configuration below to your Babel configuration file:

{
  "presets": ["@babel/preset-env"],
  "plugins": [
    [
      "@babel/plugin-transform-react-jsx",
      {
        "runtime": "automatic",
        "importSource": "react"
      }
    ]
  ]
}

Function overview

compileTemplate

Compiles a component and spits out an HTML string.

A function that compiles a React component into a plain HTML string. This is simply a wrapper over react-dom/server's renderToStaticMarkup function, allowing us to specify before and after compilation hooks, which also power the magic components.

Arguments

  • element: A ReactElement or a function returning one e.g. <Template /> or () => <Template />
  • compileOptions: An object taking various compilation options:
    • addDocType: Whether to add the <!doctype html> string at the start of the output (default: false)
    • pretty: Whether to format the output with Prettier (default: false)
    • prettyOptions: Custom options for Prettier (see this page) (default: { parser: 'html' })

Note: If you specify both addDocType: true and pretty: true the doctype will be formatted as <!DOCTYPE html>

import type { FunctionComponent } from 'react';
import LogicfulTemplates from 'logicful-templates';

const Template: FunctionComponent<{}> = () => (
  <div className="greeting">Hello World</div>
);

const result = LogicfulTemplates.compileTemplate(<Template />);
// result: <div class="greeting">Hello World</div>

// Write output to a file
fs.writeFile("template.html", result, () => {});

registerHook

Register a before or after hook to execute during compilation.

For example:

  • Setting addDocType to true registers an after hook, ensuring the output starts with <!doctype html>
  • Setting pretty to true registers an after hook, tidying up HTML before returning it.

You can find the source of above hooks below:

Internally hooks are being used to make <Magic> and <Comment> components possible, they simply render a placeholder which is then replaced when the after hook is executed. You can check out their implementation below:

If you have a good idea for other hooks, PRs are more than welcome. πŸš€

Registering a before hook

before hooks execute before calling renderToStaticMarkup, they aren't called with any parameters, but they allow you to execute any type of logic before compilation starts. For example:

LogicfulTemplates.registerHook('before', () => {
  console.log('Starting compilation...');
});

Registering an after hook

after hooks execute after calling renderToStaticMarkup, they are called with the output of either that call, or the output of the previously executed after hook. For example:

LogicfulTemplates.registerHook('after', (html) => {
  return html.toUpperCase();
});

Notes

  • Hooks are executed and then disposed of, if you're calling LogicfulTemplates.compileTemplate multiple times and want to re-use the hooks, you'll have to register them again.

Special components

<Magic> πŸ§™

This component allows you to perform magic that normally wouldn't work when using React regularly.

prop type description
compileLater boolean OR number Allows you to compile the component after everything else is compiled. If a boolean is passed, it will be rendered after every other regular component. If you have multiple <Magic compileLater> components, the regular order is maintained (i.e. based on which one is executed first). If a number is passed, the number will be considered as the priority of when to render the component, priority 1 is considered higher than priority 99.
children ReactNode React children
dangerouslySetInnerHTML { __html: string } Sets this element's innerHTML (which becomes outerHTML as <Magic> itself does not render an element)
hoist boolean Allows you to hoist its children up a level, i.e. you could set this element's outerHTML

Compiling a component later

This could be useful when you need to execute a function only at the end of compilation.

import { Magic } from 'logicful-templates';
import type { FunctionComponent } from 'react';

const Template: FunctionComponent<{}> = () => {
  return (
    <html>
      <head>
        <meta charSet='utf-8' />
      </head>
      <body>
        <div className="header">
          <Magic compileLater>
            {() => {
              executeSomeFunctionLast();
              const header = 'I am executed last';
              return <h1>{header}</h1>
            }}
          </Magic>
        </div>
        <div className="content">
          <p>Some text goes here.</p>
        </div>
      </body>
    </html>
  );
}

You can also specify a priority level by passing a number instead of true, the "compiler" will respect these values and execute the <Magic compileLater={number}> components in the correct order.

import { Magic } from 'logicful-templates';
import type { FunctionComponent } from 'react';

const Template: FunctionComponent<{}> = () => {
  return (
    <Magic compileLater={2}>
      {() => {
        executeSomeFunction();
        return <h1>I am executed 2nd!</h1>
      }}
    </Magic>
    <Magic compileLater={1}>
      {() => {
        executeSomeFunction();
        return <h1>I am executed 1st!</h1>
      }}
    </Magic>
  );
}

A real life scenario could be to gather a bunch of JSS styles that are generated on a component's first render.

Notes

  • You are not required to pass a function as the <Magic> component's children, but they allow you to execute functions. Passing regular children (i.e. React elements) will work fine.
  • If you return another <Magic> component within a child render function of another <Magic> component, the priority number will only count for that level of <Magic> components (think JavaScript event loop). You normally wouldn't run into this scenario.

Hoisting a component / setting outerHTML

By itself <Magic hoist> isn't very useful, but combined with the dangerouslySetInnerHTML prop you can technically set this element's outerHTML. This could be useful if you need to inline non-standard bits into the DOM, for example a stylesheet, custom elements, or HTML comments.

import { Magic } from 'logicful-templates';
import type { FunctionComponent } from 'react';

const Template: FunctionComponent<{}> = () => {
  return (
    <>
      <Magic hoist dangerouslySetInnerHTML={{ __html: '<!-- A stylesheet below -->' }} />
      <style>
        <Magic
          hoist
          dangerouslySetInnerHTML={{
            __html: `
          body {
            background-color: linen;
          }

          h1 {
            color: maroon;
            margin-left: 40px;
          }
        `,
          }}
        />
      </style>
    </>
  );
};
<!-- A stylesheet below -->
<style>
  body {
    background-color: linen;
  }

  h1 {
    color: maroon;
    margin-left: 40px;
  }
</style>

<Custom>

Provides flexibility by allowing you to specify what the output tag name for an element will be, while also allowing you to specify any type of prop (or attribute) on the element.

prop type description
tagName string (lowercased) A lowercased custom tag name for the custom element
[key: string] any The <Custom> component accepts any type of props

For example:

import { Custom } from 'logicful-templates';
import type { FunctionComponent } from 'react'

const MyComponent: FunctionComponent<{}> = () => {
  return (
    <Custom
      tagName='amp-img'
      alt='A view of the sea'
      src='/path/to/img'
      width={900}
      height={675}
      layout='responsive'
    />
  )
}
<amp-img
  alt="a view of the sea"
  src="/path/to/img"
  width="900"
  height="675"
  layout="responsive"
></amp-img>

You could write an abstraction over the <Custom> component to provide better type hinting for the next person consuming your component. For example:

import { Custom } from 'logicful-templates';
import type { FunctionComponent } from 'react';

interface AmpImgProps {
  alt: string;
  src: string;
  width: number;
  height: number;
  layout: string;
}

const AmpImg: FunctionComponent<AmpImgProps> = (props) => {
  return <Custom tagName='amp-img' {...props} />;
};

<Comment>

Provides a way of adding HTML comments to the compiled output.

prop type description
children string OR number OR boolean The content of the HTML comment.
import { Comment } from 'logicful-templates';
import type { FunctionComponent } from 'react';

const MyComponent: FunctionComponent<{}> = () => {
  const input = 'World';

  return (
    <div>
      <Comment>Hello {input}</Comment>
    </div>
  )
};
<div>
  <!-- Hello World -->
</div>

Bring your own types

You could easily extend the default JSX types by defining your own types in a separate typings.d.ts file.

  • Note: If you're using ts-node to compile your templates, make sure to set { "ts-node": { "files": true } in your tsconfig.json, else you may run into TypeScript errors.
export {}

declare global {
  namespace JSX {
    interface IntrinsicElements {
      // Add a new element:
      'custom-element': {
        [key: string]: any;
      };
      // Or extend an existing React element:
      html: React.DetailedHTMLProps<React.HTMLAttributes<HTMLHtmlElement>, HTMLHtmlElement> & {
        amp4email: ""
      }
    }
  }
}

About

A wrapper over React's renderToStaticMarkup for building logicful HTML templates, using JSX, transform hooks, and a hint of magic.

Topics

Resources

License

Stars

Watchers

Forks

Sponsor this project

 

Languages