Giter Club home page Giter Club logo

logicful-templates's Introduction

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: ""
      }
    }
  }
}

logicful-templates's People

Contributors

aaimio avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

logicful-templates's Issues

allow hooks to be passed as options

Rather than calling LogicfulTemplates.registerHook(myHookFn) hooks should be passable to the LogicfulTemplates.compileTemplate() function, e.g.:

LogicfulTemplates.compileTemplate(<Template />, {
  addDocType: true,
  pretty: true,
  beforeHooks: [],
  afterHooks: [
    myCustomHookFn,
    anotherCustomHookFn,
    (html: string) => {
      return html.toUpperCase();
    }
  ]
})

This makes more sense as registerHooks kind of suggests they'll be sticking around rather than being disposed of after execution.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.