1.3.3 • Published 6 months ago

feather-render v1.3.3

Weekly downloads
-
License
ISC
Repository
github
Last release
6 months ago

Feather Render

gzip license version

✨ A feather light render framework ✨ 721 bytes minified and gzipped - no dependencies - SSR support

Companion framework:

Live examples:

coffee

Getting started

Package

npm i feather-render

...or inline

<head>
  <script src="feather-render.min.js"></script>
</head>
<body>
  <script>
    const { html, hydrate } = window.__feather__ || {};
  </script>
</body>

Index

Usage

Documentation

Definitions

Examples

Usage

Basic syntax

import { FR, html } from 'feather-render';
import { TodoItemProps } from './TodoItem.types';

const TodoItem: FR<TodoItemProps> = ({ todo }) => {
  return html`
    <li>${todo.title}</li>
  `;
};

const TodoList: FR = () => {
  return html`
    <ul>${todos.map(todo => TodoItem({ todo }))}</ul>
  `;
};

const Document: FR = () => html`
  <!DOCTYPE html>
  <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Feather</title>
      <script type="module" src="index.mjs"></script>
    </head>
    <body>
      ${TodoList()}
    </body>
  </html>
`;

Tip: Plugins for VSCode like lit-html or Inline HTML can be used for syntax highlighting.

Server-Side Rendering (SSR)

import express from 'express';
import { Document } from './components/Document';

const server = express();

server.use(express.static('dist'));

server.get('/', (req, res) => {
  const document = Document({ req });
  res.send(document.toString());
});

server.listen(5000);

Client hydration

import { hydrate } from 'feather-render';
import { TodoList } from './components/TodoList.js';

hydrate(TodoList(), document.body);

Documentation

html()

const render = html`<div></div>`;

Parameters

  • string - html template string to render

Return value

html().mount()

mount(() => {
  console.log('Component inserted in DOM');
});

Parameters

  • callback() - function called when component is inserted in DOM

Return value

  • void

html().unmount()

unmount(() => {
  console.log('Component removed from DOM');
});

Parameters

  • callback() - function called after component is removed from DOM

Return value

  • void

hydrate()

hydrate(App(), document.body);

Parameters

  • element - Render from html()
  • target - where to mount the element

Return value

  • void

Definitions

Render

Return from html()

const { refs, render, element, mount, unmount } = html`<div></div>`;
  • refs - list of id'ed elements
  • render - this
  • element - element to insert in DOM
  • mount() - set callback for mount
  • unmount() - set callback for unmount

FR

Feather Render

const Page: FR<Props> = (props) => {
  return html`
    <main>
      ${props.title}
    </main>
  `;
};

FP

Feather Render Promise

const Page: FP<Props> = async (props) => {
  const pageData = await fetchPageData(props);

  return html`
    <main>
      ${pageData.title}
    </main>
  `;
};

Examples

Re-rendering

Primitive values

import { store } from 'feather-state';
import { FR, html } from 'feather-render';

const { watch, ...state } = store({
  greeting: 'Hello, World'
});

const Component: FR = () => {
  const { refs, render } = html`
    <p id="paragraph">${state.greeting}</p>
  `;

  // Watch greeting + update DOM
  watch(state, 'greeting', (next) => {
    refs.paragraph?.replaceChildren(next);
  });

  // Change greeting state
  setTimeout(() => {
    state.greeting = 'Hello, back!';
  }, 1000);

  return render;
};

Lists

import { store } from 'feather-state';
import { FR, html } from 'feather-render';
import { TodoItemProps } from './TodoItem.types';

const { watch, ...state  } = store({
  todos: ['Todo 1', 'Todo 2'];
});

const TodoItem: FR<TodoItemProps> = ({ todo }) => {
  return html`
    <li>${todo}</ul>
  `;
};

const TodoList: FR = () => {
  const { refs, render } = html`
    <ul id="todoList">
      ${state.todos.map(todo => (
        TodoItem({ todo })
      ))}
    </ul>
  `;

  const reRenderTodos = () => {
    const fragment = new DocumentFragment();
    for (let todo of todoStore.todos) {
      const { element } = TodoItem({ todo });
      element && fragment.appendChild(element);
    }
    refs.todoList?.replaceChildren(fragment);
  };

  // Watch todos + update DOM
  watch(state, 'todos', () => {
    reRenderTodos();
  });

  // Append todo in state
  setTimeout(() => {
    state.todos = [...state.todos, 'Todo 3'];
  }, 1000);

  return render;
};

Event listeners

Form submission

import { FR, html } from 'feather-render';

const Component: FR = () => {
  const { refs, render, mount, unmount } = html`
    <form id="form">
      <p id="status">Fill in form</p>
      <input type="text" />
      <button type="submit">Submit</button>
    </form>
  `;

  const handleSubmit = (event) => {
    event.preventDefault();
    refs.status?.replaceChildren('Submitting');
  };

  mount(() => {
    refs.form?.addEventListener('submit', handleSubmit);
  });
  unmount(() => {
    refs.form?.removeEventListener('submit', handleSubmit);
  });

  return render;
};

Async components

Server

import express from 'express';
import { Document } from './components/Document';

const server = express();

server.get('/', async (req, res) => {
  const document = await Document({ req });
  res.send(document.toString());
});

server.listen(5000);

Client hydration

import { hydrate } from 'feather-render';
import { fetchPage } from './Document.helpers';

fetchPage('/').then(page => {
  hydrate(page, document.body);
});

Document helper

import { Render } from 'feather-render';
import { ErrorPage } from './ErrorPage';
import { Page } from './Page';

export const fetchPage = async (path: string): Promise<Render> => {
  try {
    const pageData = await (await fetch(`/api/page${path}`)).json();
    return pageData ? Page({ pageData }) : ErrorPage({ code: 404 });
  } catch {
    return ErrorPage({ code: 500 });
  }
};

Document

import { Request } from 'express';
import { FP, html } from 'feather-render';
import { fetchPage } from './Document.helpers';

type Props = {
  req: Request;
};

const Document: FP<Props> = async ({ req }) => html`
  <!doctype html>
  <html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Feather</title>
    <script type="module" src="/index.mjs"></script>
  </head>
  <body>
    ${await fetchPage({ path: req.path })}
  </body>
  </html>
`;

Fetching

Server and client

import { FR, html } from 'feather-render';

const App: FR = () => {
  const { render } = html``;

  fetch('http://localhost:5000/api/v1/user')
    .then(res => res.json())
    .then(res => console.log(res));

  return render;
};

Server or client

import { FR, html } from 'feather-render';

const isServer = () => typeof window === 'undefined';
const isClient = () => typeof window !== 'undefined';

const App: FR = () => {
  const { render } = html``;

  if (isServer()) {
    fetch('http://localhost:5000/api/v1/user')
      .then(res => res.json())
      .then(res => console.log(res));
  }

  if (isClient()) {
    fetch('http://localhost:5000/api/v1/user')
      .then(res => res.json())
      .then(res => console.log(res));
  }

  return render;
};

On mount

import { FR, html } from 'feather-render';

const App: FR = () => {
  const { render, mount } = html``;

  mount(() => {
    fetch('http://localhost:5000/api/v1/user')
      .then(res => res.json())
      .then(res => console.log(res));
  });

  return render;
};

Lazy / Suspense

import { FR, html } from 'feather-render';

const App: FR = () => {
  const { render } = html`
    <div id="lazyParent"></div>
  `;

  import('./LazyComponent').then(({ LazyComponent }) => {
    const { element } = LazyComponent();
    element && refs.lazyParent?.replaceChildren(element);
  });

  return render;
};

Unique id's

let i = 0;
export function id(name: string) {
  return `${name}_${i++}`;
}
import { FR, html } from 'feather-render';
import { id } from '../helpers/id';

const App: FR = () => {
  const uniqueId = id('unique');

  const { refs, render, mount } = html`
    <div id=${uniqueId}></div>
  `;

  mount(() => {
    refs[uniqueId]?.replaceChildren('Component mounted');
  });

  return render;
};

CSS in JS

Components

import { FR, html } from 'feather-render';
import { css } from '@emotion/css';

const Page: FR = () => html`
  <main class=${css`background: red;`}>
  </main>
`;

Server-Side Rendering (SSR)

import { FR, html } from 'feather-render';
import createEmotionServer from '@emotion/server/create-instance';
import { cache } from '@emotion/css';
import { Page } from './Page';

const { extractCriticalToChunks, constructStyleTagsFromChunks } = createEmotionServer(cache);

const Document: FR = () => {
  const page = Page();
  const chunks = extractCriticalToChunks(page.toString());
  const styles = constructStyleTagsFromChunks(chunks);

  return html`
    <!doctype html>
    <html lang="en">
    <head>
      <meta charset="UTF-8" />
      <meta http-equiv="X-UA-Compatible" content="IE=edge" />
      <meta name="viewport" content="width=device-width, initial-scale=1.0" />
      <title>Feather</title>
      <script type="module" src="/index.mjs"></script>
      ${styles}
    </head>
    <body>
      ${page}
    </body>
    </html>
  `;
};

Roadmap 🚀

  • CLI tool
  • Cleaner way of referencing values in html
  • Binding values, re-renders and listeners
  • Router example
1.3.3

6 months ago

1.3.2

6 months ago

1.3.1

6 months ago

1.2.0

7 months ago

1.2.3

7 months ago

1.2.2

7 months ago

1.3.0

7 months ago

1.2.1

7 months ago

1.1.10

7 months ago

1.1.9

1 year ago

1.1.8

2 years ago

1.1.7

2 years ago

1.1.6

2 years ago

1.1.5

2 years ago

1.1.4

2 years ago

1.1.3

2 years ago

1.1.1

2 years ago

1.1.0

2 years ago

1.1.2

2 years ago

1.0.2

2 years ago

1.0.1

2 years ago

1.0.0

2 years ago

0.1.0

2 years ago