1.3.3 • Published 6 months ago
feather-render v1.3.3
Feather Render
✨ A feather light render framework ✨ 721 bytes minified and gzipped - no dependencies - SSR support
Companion framework:
Live examples:
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
Return value
void
Definitions
Render
Return from html()
const { refs, render, element, mount, unmount } = html`<div></div>`;
refs
- list of id'ed elementsrender
-this
element
- element to insert in DOMmount()
- set callback for mountunmount()
- 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
- Event listeners
- Fetching
- Other
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