README
EZT
Template based web development library.
EZT means "Easy Template", It's:
- Easy to use. It's built on
lodash.template
andrxjs
. - Small. 8.5kB gzipped.
- Available in both server side and frontend.
- Available in IE10+.
Installation
npm install ezt
Get Started
import ezt from "ezt";
const data = [{ id: 1, title: "Make breakfast.", completed: false }];
const itemComponent = ezt("<li><%= todo %></li>");
const listComponent = ezt({
template: `
<ul id="list">
<% for (var i in $) { %>
<%= $[i] %>
<% } %>
</ul>`,
children: data => {
return data.map(item => ({
data: item,
fn: itemComponent
}));
}
});
console.log(listComponent(todos)); // Will print the list.
Why?
Template engines works well on producing HTML strings, but they don't define behavior(event listeners, animations, etc.) of a web app. Frontend frameworks based on virtual DOM provide declarative, state-driven UI development, but state management is not easy, and the performance of server side rendering is not so satisfying.
Assume this situation:
- SEO is important for you.
- Your site has intensive user interractions and UI effects.
- Business logic of your site is complicated, and there may be a lot more modules in future.
So we need to:
- Render HTML with high performance on server side.
- Declare UI and interaction of our web app.
- Build flexible and maintainable business logic.
How to?
1. Render HTML with high performance on server side.
On server side, components are just template functions. Components without children can be declared like this:
const itemComponent = ezt("<li><%= todo %></li>");
or this:
const itemComponent = ezt({ template: "<li><%= todo %></li>" });
To declare components with children, we need a children
method:
const listComponent = ezt({
template: `
<ul id="list">
<% for (var i in $) { %>
<%= $[i] %>
<% } %>
</ul>`,
children: data => {
return data.map(item => ({
data: item,
fn: itemComponent
}));
}
});
Method chidren
takes a data object, returns an Array
or an Object
with each item in it has two property: data
(data passed to the child) and fn
(the child component).
In the template, dollar sign $
refers to templates of the children. In the example above, $[0]
is <li>Make breakfast.</li>
Since components are just template functions, we can import them and generate HTML easily:
const express = require("express");
const listComponent = require("./dist/components/listComponent"); // transpiled component
const app = express();
app.get("/todos", (req, res) => {
const data = [{ id: 1, title: "Make breakfast.", completed: false }];
const listHtml = listComponent(data);
res.set("Content-Type", "text/html");
res.send(`
<!doctype html>
<html lang="en">
<head></head>
<body>
${listHtml}
</body>
</html>
`);
});
app.listen(3000);
2. Declare UI and interaction of our web app.
EZT uses lodash.template
and UI declaration is just the same:
let t = ezt("hello <%= name %>!");
t({ name: "John" }); // "hello John!"
t = ezt("<b><%- value %></b>");
t({ value: "<script>" }); // "<b><script></b>"
t = ezt('<%= "\\<%- value %\\>" %>');
t({ value: "This value will be ignored" }); // "<%- value %>"
for more examples, you can check lodash docs.
EZT is based no template, not virtual DOM, so there is no setState
. EZT adopts traditional MVC instead of state-centralized
patterns. Components are in view layer, they can either dispatch an action or respond to data change. Let's make the example above more practical, if we want to remove a todo item:
// components/itemComponent.js
import { dispatch, respondTo } from "ezt";
const itemComponent = ezt({
template: `
<li>
<span>
<%= id %>.<%= title %>
</span>
<button data-ref="btn">Remove</button>
</li>`,
init(data, el, refs) {
const subscriptions = {};
refs.btn.addEventListener("click", () => {
dispatch("removeItem", data.id);
});
subscriptions.onRemove = respondTo("itemRemoved", onRemove);
function onRemove(id) {
if (id === data.id) {
subscriptions.onRemove.unsubscribe();
el.remove();
}
}
}
});
export default itemComponent;
The init
method is used to define the client side behavior of components. data
is the data object passed to the component, el
is the DOM element of the component, and refs
is the reference to the DOM elements with attribute data-ref
.
When rerendering, DOM manipulation is still needed, so libraries like jQuery
or DOM7
are recommended.
Why we still choose to manipulate DOM by hand?
With components, most of the time DOM manipulations are local(deal with the component itself) and not complicated.
Template engines don't deal with event listeners, we have to add them in
init
method.Vanilla JS has better performance and is easier to optimize.
No concerns on
componentShouldUpdate
,forceUpdate
...Subscriptions are more flexible than component lifecycle hooks.
Now let's handle the actions dispatched by itemComponent
// controllers/listController.js
import { Controller } from "ezt";
import listModel from "../models/listModel";
class ListController extends Controller {
removeItem(id) {
if (listModel.removeTodoItemById(id)) {
this.respond("itemRemoved", id);
}
}
}
export default new ListController();
Actions dispatched by components will be handled by corresponding methods of controllers. Controllers' methods are bind to themselves, so we don't have to write removeItemById = id => {...}
.
Models can be simple objects:
// models/listModel.js
class ListModel {
todos = [];
constructor(todos) {
this.todos = todos;
}
removeTodoItemById(id) {
const index = this.todos.findIndex(item => item.id === id);
return index > -1 ? this.todos.splice(index, 1) : null;
}
}
export default new ListModel([{ id: 1, title: "Make breakfast.", completed: false }]);
Then we can run our app:
// main.js
import listModel from "./models/listModel";
import listComponent from "./components/listComponent";
import listController from "./controllers/listController";
listController.bind();
listComponent(listModel.todos, document.getElementById("list"));
3. Build flexible and maintainable business logic.
Domain Driven Design(DDD) is recommended. We can have multiple domains(models) and each domain provides methods to manipulate data. Ajax requests are sent in controllers.
// models/listModel.js
class listModel {
...
toggleTodoItem(id) {
const item = this.todos.find(todo => todo.id === id);
if (item) {
item.completed = !item.completed;
return item;
}
return null;
}
}
// controllers/listController.js
import logModel from "../models/logModel";
class ListController {
...
toggleItem(id) {
/* Assume we've added another module named logModel */
const log = logModel.getLogContent(id);
return http.post("path/to/log/system", log).then(res => {
console.log("log uploaded");
const item = listModel.toggleTodoItem(id);
this.respond("itemToggled", item);
});
}
}
We can tear down our business logic into multiple modules and make our app maintainable.
Docs
- Component
- ComponentOptions
- Controller
- createComponent
- dispatch
- ezt
- LazyComponent
- respondTo
- TemplateOptions
ezt.Component
interface Component {
(data: { [k: string]: any }, element?: null | HTMLElement): string | HTMLElement;
}
ezt.ComponentOptions
interface ComponentOptions {
template: string;
templateOptions?: TemplateOptions;
children?: (data: { [k: string]: any }) => { [k: string]: LazyComponent } | Array<LazyComponent>;
init?: (data: { [k: string]: any }, el: HTMLElement, refs: { [k: string]: HTMLElement }) => void;
}
ezt.Controller
import { Subscription, UnaryFunction } from "rxjs";
abstract class Controller {
_subscriptions: Array<Subscription>;
bind(): void;
unbind(): void;
dispatch(name: string, args: any): Controller;
respond(name: string, args: any): Controller;
_on(
actionName: string,
pipes: () => void | UnaryFunction<any, any> | UnaryFunction<any, any>[],
handler?: () => void
): Controller;
}
Example:
import { throttleTime, delay } from "rxjs/operators";
import { Controller, dispatch, respondTo } from "ezt";
class MyController extends Controller {
log(content) {
console.log("myController.log: " + content);
}
}
const myController = new MyController();
myController.bind(); // myController is listening to actions now
dispatch("log", "testing log"); // myController.log: testing log
respondTo("logged", content => {
console.log("respondTo.logged: " + content);
});
myController.respond("logged", "records logged"); // respondTo.logged: records logged
myController.unbind(); // myController is not listening to actions any more
dispatch("log", "testing log"); // Won't print
// Still able to send responses
myController.respond("logged", "records logged"); // respondTo.logged: records logged
// We can add pipes for methods
myController.log.pipes = throttleTime(100);
// Multiple pipes
myController.log.pipes = [throttleTime(100), delay(20)];
ezt.createComponent
See ezt
.
ezt.dispatch
function dispatch(type: string, args: any): void;
ezt
function ezt(options: string | ComponentOptions): Component;
Example:
const greetingDiv = ezt({
template: "<div>Hello <%= name %></div>",
init(data, el) {
el.addEventListener("click", () => {
el.textContent = 😀;
});
}
});
greetDiv("Mike"); // <div>Hello Mike</div>
greetingDiv("Mike", null) // Will produce a new HTMLDivElement. Use this when we want to create a new component in browser.
greetingDiv("Mike", document.getElementById("greeting-div")); // Will call init method on the selected div.
ezt.LazyComponent
interface LazyComponent {
data: { [k: string]: any };
fn: Component;
}
ezt.respondTo
import { Subscription, UnaryFunction } from "rxjs";
function respondTo(
reaction: string,
pipes: () => void | UnaryFunction<any, any> | UnaryFunction<any, any>[],
handler?: () => void
): Subscription;
Example:
import { throttleTime, delay } from "rxjs/operators";
const subscriptions1 = respondTo("itemRemoved", id => {
console.log(`item with id = ${id} is removed`);
});
subscriptions1.unsubscribe();
/* We can add pipes */
const subscriptions2 = respondTo("itemRemoved", throttleTime(100), id => {
console.log(`item with id = ${id} is removed`);
});
subscription2.unsubscribe();
/* Multiple pipes */
const subscriptions3 = respondTo("itemRemoved", [throttleTime(100), delay(20)], id => {
console.log(`item with id = ${id} is removed`);
});
subscription3.unsubscribe();
ezt.TemplateOptions
interface TemplateOptions {
escape?: RegExp;
evaluate?: RegExp;
imports?: { [k: string]: any };
interpolate?: RegExp;
sourceURL?: string;
variable?: string;
}