You Could be Building a Web app Without Writing any New HTML or CSS code. 😳 🤯
Tim Bendt
Posted on January 24, 2019
I have been writing a lot of web apps for a long time. Over the past year or so I've been slowly developing a functional JavaScript library for browser-based-apps. I was inspired by the wide variety of virtual-dom based libraries, and the concepts of functional reactive programming. Specifically there is a pattern that captured my imagination called SAM.
SAM stands for State Action Model. And it's a way of constructing your app that works really well with simple vanilla JS. I wanted to write something using this pattern but with the additional help of TypeScript.
I like the idea of building something from first principles to understand what's really going on. So I started looking into how these libraries like React, Vue, Preact, and Mithril work. I discovered that you don't have to use JSX or a template compiler, you can just write hyperscript functions to render your UI. I found a lightweight standalone virtualdom implemented in TypeScript and started wrapping some helpers around it to make the SAM pattern easy to follow.
Thus was born FRETS.
https://github.com/sirtimbly/frets#readme
If you want to see what a FRETS app looks like in action then check out this little Demo. It demonstrates the basic functionality of navigation, validation, and async updates.
Take a look at the code in the frets-starter app to see how it's all pieced together. UI Components are pure functions. They simply return a bunch of VNodes (Virtual Dom Nodes) for the virtual-dom rendering layer to use for diffing. Functions are superior to objects and template strings because they are easy to compose and refactor. (Especially with TypeScript).
The user interface is a pure function with no side-effects because it is only responsible for expressing the contents of the modelProps object, which is frozen and marked as Readonly
for strictness whenever a developer is expected to interact with it. This is one area where Typescript can really help prevent deep modification and accidental side-effects.
Atomic CSS transformed into JS
When you see how these UI rendering methods are written you'll notice they don't look like normal hyperscript functions.
import { $, $$ } from "./base-styles.ts";
export const grayBox = (childNodes) => $.div.p2.m1.border.rounded.bgLightyGray.h(childNodes);
Instead of passing the tag name and class names as strings, I created a tool that analyzes any CSS file and generates a typescript file full of chainable helper methods to generate hyperscript functions that can only implement a strict set of classes. This approach perfectly fits the atomic CSS libraries that provide a huge collection of classes that apply exactly one visual CSS property. I like using Tachyons or it's lighter ancestor Bass CSS. The api is fluent, every time you hit '.' you get auto-completion listing all the available CSS properties to add. And when you're done chainging Atomic CSS classes you call the .h()
method and pass in the properties and childnodes like you would with any other hyperscript function.
So the end result of this is that you're not writing anything that looks like HTML syntax or CSS syntax for your app. You're just writing functions that create HTML in the Dom and using developer friendly tools to help you find and use the CSS classes available to you for visual formatting. It's like an evolution of CSS in JS, but without the JS ever needing to parse or create CSS during runtime.
I like HTML and CSS. I've been building websites for almost 20 years now. I know CSS very well, and I do not hate it. I just think it's interesting to try something new, and as a developer I enjoy the way my IDE helps me write very functional and composable code without having to do any template interpretation.
State
The state in FRETS is a custom class that you pass into the FRETS constructor. Your UI will be a pure expression of this immutable object.
export class TodoListProps extends PropsWithFields {
public name: string;
public email: string;
public todos: ITodo[];
public complete: number;
constructor() {
// set some defaults
}
}
Actions
Actions are what respond to user interactions and events. When someone clicks something or types into a field the state has to be updated. Actions are responsible for this. First you write an empty class to specify your action names.
export class TodoListActions extends ActionsWithFields {
public saveName: (e: Event) => void;
// and the rest with this same type signature
}
You have to then specify your concrete implementation on the FRETS object after it's been initialized.
const F = new FRETS<TodoListProps, TodoListActions>(new TodoListProps(), new TodoListActions());
F.actions.changeName = F.registerAction((e: Event, data: TodoListProps) => {
data.name = (e.target as HTMLInputElement).value;
// add action specific business logic here, but not validation
// also you can add 3rd party API calls here
return data;
});
Any actions that kick off an async function can call F.render()
inside it's promise .then
handler.
Model
The Model is responsible for handling, validating and calculating the proposed new properties that the "actions" return. This is done through two functions you specify on your app, validator
and calculator
.
// Register the state calculation function
F.calculator = (newProps: TodoListProps, oldProps: TodoListProps): TodoListProps => {
// add your derived state business logic here
const completedCount = newProps.todos.filter(t => (t.completed === true)).length;
return {...newProps, completed: completedCount };
};
All changes to the UI that is displayed have to be based up the values that are finally returned from the "calculator" method. Which get stored on F.modelProps
and are accessible from inside the UI rendering methods because they receive a reference to the App as an argument every time rendering occurs (automatically after any action is triggered).
All of this working together creates a predictable loop of logic that should be easy to reason about for a developer. You always know exactly where your business logic is: mutations are happening inside an action, and derived values are inside the calculator function. Rendering logic is always in the rendering functions, and you should be able to reason about how the UI got into the state it is at any point in the application's lifecycle.
There's a lot more about this in the API docs. And some more friendly docs are coming soon. Thanks!
Posted on January 24, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
January 24, 2019