Table of Contents
- Introduction
- Component Based Design
- Registering Single File Components with Peasy
- Demo Application
- Demo Component Implementation
- More information
- Conclusion
Introduction
Today we are going to dive into the final layer of the Peasy-UI library. We are going to do a deep dive into how to use Peasy-UI to build re-usable UI components. This will assist with building modular, scalable, re-usable code blocks and simplify one's code architecture.
In this article, we will review the benefits of component based architecture, the different component features of the Peasy-UI library, specifically single-file components that can be done in JavaScript or HTML, and then we will wrap up with a dive into a demo application to reinforce many of the ideas introduced today.
If you missed the introduction article for Peasy-UI, you can read it here, and it can provide the overview for the library.
Component Based Design
Larger applications need to be architected in a manner that keeps the code base clean and lean, in a manner where duplicate code isn't being used. One strategy to help with a "don't repeat yourself" approach is to design your front-end in modular manner, and design that structure to re-use components. Component based architecture provides increased flexibility, improved maintainability, increased readability, and also improves organization of a code base.
Registering Single File Components with Peasy
Peasy UI makes it possible to create both JavaScript and HTML single file components and import them for use in the app, without the need of a build step. You also can use this with Typescript when you bundle with your preferred bundling tool that transpiles your TS files into JS. Peasy-UI only needs a template property in order to render a component, but for Peasy-UI to instantiate and render components based on a template object/class and (optionally) data, the template object/class needs:
- a template property
- a create method (which gets invoked with designated data model)
- to be known to the parent model either as a property or through the use of the UI.register method
JavaScript (TypeScript) Single File Component
There are two ways of fulfilling the 3rd requirement above, providing the component to the parent model. You can either use UI.register() method, or include the exported class into the parent data model yourself. For this example, we will use the UI.register(), but in the demo application below, we demonstrate the other method of meeting this requirement.
// list-item.js
import { UI } from "https://cdn.skypack.dev/@peasy-lib/peasy-ui";
export class ListItem {
static template = "<span>Hello, ${name}!</span>";
constructor(name) {
this.name = name;
}
static create(state) {
return new ListItem(state.name);
}
}
UI.register("ListItem", ListItem);
Then we can import it into our HTML and implement it, the below example demonstrates how to bypass the build step requirement, by way of CDN import.
<!-- index.html -->
<body>
<template id="sfc-app">
<div>
<div pui="name <=* names"><list-item pui="ListItem === name"></list-item>
</div>
</template>
<script type="module">
import { UI } from "https://cdn.skypack.dev/@peasy-lib/peasy-ui";
import { ListItem } from './list-item.js';
class SFCApp {
names = [{ name: 'World' }, { name: 'everyone' }];
}
UI.create(document.body, '#sfc-app', new SFCApp());
</script>
HTML Single File Component
You can also create your single file components using the HTML format as well. The one benefit from this approach is that your template html doesn't have to be a string literal anymore, and can be use any IDE syntax highlighting or autocomplete as a useful tool.
<!-- list-item.html -->
<style>
list-item {
color: gold;
}
</style>
<template id="list-item">
<span>Hello, ${name}!</span>
</template>
<script type="module">
import { UI } from "https://cdn.skypack.dev/@peasy-lib/peasy-ui";
export class ListItem {
static template = document.querySelector("#list-item"); // This can also be a string
constructor(name) {
this.name = name;
}
static create(state) {
return new ListItem(state.name);
}
}
UI.register("ListItem", ListItem); // Is necessary for HTML single file components
</script>
Since HTML imports aren't here (yet), Peasy UI uses the UI.import() and UI.ready() methods to support HTML single file components. From a templating perspective, there's no difference between JavaScript and HTML single file components and the two types can co-exist and be used in the same app. So the implementation details of the HTML component is:
<!-- index.html -->
<head>
<script type="module">
import { UI } from "https://cdn.skypack.dev/@peasy-lib/peasy-ui";
UI.initialize(); // UI.initialize is necessary with HTML components and needs to be in a head in top file
</script>
</head>
<body>
<template id="sfc-app">
<div>
<div pui="name <=* names"><list-item pui="ListItem === name"></list-item></div>
</div>
</template>
<script type="module">
import { UI } from "https://cdn.skypack.dev/@peasy-lib/peasy-ui";
UI.import("./list-item.html");
class SFCApp {
names = [{ name: "World" }, { name: "everyone" }];
}
await UI.ready(); // Awaiting UI.ready is necessary with HTML imports
UI.create(document.body, "#sfc-app", new SFCApp());
</script>
</body>
Going forward, we will dig into a small demonstration app, but it will be using Typescript for its components, and is using Vite as a build tool and bundler.
Demo Application
Link to application GitHub repo
The demo that's shown here is a very simple app to provide some context on how the Peasy-UI library's component features are utilized.
This demo features two component types, a Label and a Button. The button component is used twice, and is passed different parameters to control not only its behavior but its aesthetic.
Label Component
// ./src/components/label.ts
export type LabelState = {
count: number;
className: string;
};
export class Label {
// HTML template
// this is the HTML that get's rendered for the component - required for Peasy-UI components
public static template = `
<style>
.label_Component {
font-size: 3em;
color: whitesmoke;
}
</style>
<div class="label_Component \${className}">\${count}</div>
`;
// internal method that manipulates state
increment {
this.count++;
};
// internal method that manipulates state
decrement {
if (this.count > 0) this.count--;
};
// constructor that also defines the component state variables by the public keyword
constructor(public count: number, public className: string) {}
// static method that creates an instance of the class - required for Peasy-UI components
// but this is all under the hood, it becomes a UIView in your data model
static create(state: LabelState) {
return new Label(state.count, state.className);
}
}
The Label is a simple component that displays and controls the count value within its own DOM element. In the template literal you can see that it owns its own styling, plus its own DOM element, which has two Peasy-UI bindings in it. First, the public classname
is used to add a secondary CSS class to a label if necessary, in this example it isn't used, but added to show that capability. The second binding is the public count
value which is bound to the component div element.
There are two internal methods, increment() and decrement(), that are used to control the count state, and these provide external access for other components to call to manipulate the count value. This removes the need to modify the count value directly, and let's the component manage that.
Also note the create method, which is required functionality to be a Peasy-UI component. Also I define the incoming state, since I'm using Typescript, to improve the development experience on the parent side to ensure that I'm using the component properly. This isn't required, but a nice to have feature.
All aspects of the label's appearance and behaviors are captured in this one file.
Button Component
/// ./src/components/button.ts
export type ButtonState = {
buttonText: string;
className: string;
buttonClickHandler: (event: Event, model: any, elem: HTMLElement) => void;
};
export class Button {
// HTML template
// this is the HTML that get's rendered for the component - required for Peasy-UI components
public static template = `
<style>
.button_Component {
font-size: 1em;
border-radius: 10px;
border: 0px;
width: 60px;
height: 25px;
margin: 5px;
font-weight: bold;
color: #111111;
background-color: whitesmoke;
}
.button_Component:active {
-webkit-box-shadow: inset 0px 0px 5px #c1c1c1;
-moz-box-shadow: inset 0px 0px 5px #c1c1c1;
box-shadow: inset 0px 0px 5px #c1c1c1;
outline: none;
}
.up {
color: whitesmoke;
background-color: green;
}
.down {
color: whitesmoke;
background-color: red;
}
</style>
<button class="button_Component \${className}" \${click@=>buttonClickHandler}">\${buttonText}</button>
`;
// constructor that also defines the component state variables by the public keyword
constructor(
public buttonText: string,
public className: string,
public buttonClickHandler: (event: Event, model: any, elem: HTMLElement) => void
) {}
// static method that creates an instance of the class - required for Peasy-UI components
// but this is all under the hood, it becomes a UIView in your data model
static create(state: ButtonState) {
return new Button(state.buttonText, state.className, state.buttonClickHandler);
}
}
Now the button component has a little more going on than the Label component. We have similar aspects of the Label component, such as the create method, component level styling in the template literal, plus all the bindings that we have for a button. One of those bindings is unique, in that the button event callback is passed into the component so that we can use this button for different processes. This is using the event binding on the 'click' event on the DOM element. The other bindings are the button label and adding the classname
to control different styles.
Demo Component Implementation
AppUI Class
// ./src/ui.ts
import { Label, LabelState } from "./components/label";
import { Button, ButtonState } from "./components/button";
import { UIView } from "@peasy-lib/peasy-ui";
export class AppUI {
public static template = `
<div>
<div class="container">
<div class="label_container">
<!-- Using the Peasy-UI Component here for a Label-->
<\${Label:labelView=== counterState}>
</div>
<div class="button_container">
<!-- Creating two instances of the Peasy-UI Components here for buttons-->
<\${Button === upButtonState}>
<\${Button === downButtonState}>
</div>
</div>
</div>`;
// Class Properties as State below
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
// this is me fulfilling that 3rd requirement w/o the UI.register() method
public Button = Button;
public Label = Label;
// this is the assigned 'instance' of the Label component (as a UIView)
labelView: UIView | undefined = undefined;
// This is the state 'props' that are passed into the Label component
counterState: LabelState = { count: 0, className: "" };
// This is the state 'props' that are passed into each button component
upButtonState: ButtonState = {
buttonText: "Up",
className: "up",
buttonClickHandler: () => {
//reference to the internal method in the Label class
this.labelView!.model.increment();
},
};
downButtonState: ButtonState = {
buttonText: "Down",
className: "down",
buttonClickHandler: () => {
//reference to the internal method in the Label class
this.labelView!.model.decrement();
},
};
}
So with any Peasy-UI project, we have a data model that represent the application state that Peasy uses. In this implementation, we are using the AppUI
class to provide that data model. The properties of the class becomes the active 'State' for the data model. Let's go through this section by section.
Rendering Template
public static template ="...";
I like to define the template at the top of my class but that isn't necessary, its developer preference and just a habit of mine. This is the parent string template that is provided into the UIView.Create() method for rendering. This provides the HTML content that Peasy-UI will render inside the target element, in this example its document.body
. Here we can point out the bindings for the Peasy-UI components.
Let us look at the two different types of Component bindings here. Let's take the Label binding first.
<\${Label:labelView=== counterState}>
The three important ideas passed here are the class of Component: Label
, the UIView (instance) assignment :labelView
, and the state object provided: counterState
. All three of these are used with the ===
binding tag. This essentially states that we are creating a View of the Label class, using the counterState object as a property to pass into the create method, and that the returning View of the class should be saved by reference in the labelView
value. Since we are using the instance (View) value, we can then have access to all the internals of the class just like any other JS class can.
<\${Button === upButtonState}>
<\${Button === downButtonState}>
Now there are just two different aspects of the Button binding. First, we are using two instances of the binding, but are passing two different state objects, this demonstrates the 're-usability' of the component. As a reminder, this even includes us passing the event callback into the component so its behavior is provided to it.
Secondly, we are not in need of the View of the component classes, so we are not binding the return value to anything.
Class properties as State
// this is me fulfilling that 3rd requirement w/o the UI.register() method
public Button = Button;
public Label = Label;
The first two things declared in the data model are the actual component classes. This provides Peasy-UI access to the class components, and when a template binding calls these out, it uses the create method on each class to instantiate each component. This fulfills that 3rd requirement for a Peasy-UI component.
// this is the assigned 'instance' of the Label component as a UIView
labelView: UIView | undefined = undefined;
labelView
is the UIView returned when Peasy-UI uses the create method to create an instance of the Label class, then converts it to a UIView representing a Label class that is created, this gives us access to the two methods we will use to control the count on the label. We will see later in the template section how the binding to this works. Its important to note, that i call out labelView
as either a UIView | undefined
, and this is because it IS undefined until the UIView.create().attached property in the main.ts call resolves its promise. This is a means to appease Typescript type checking, but it is important to note and understand that it is undefined until the View is attached.
// main.ts
// Peasy-UI method for creating a View and attaching to DOM
await UI.create(document.body, new AppUI(), AppUI.template).attached;
It is possible to not use the attached property, but there is a risk of attempting to render something from the data model that hasn't been evaluated yet, and this pattern prevents that from occurring. Once the view is attached to the DOM, then all undefined values in the data model will be evaluated to their intended states.
// This is the state 'props' that are passed into the Label component
counterState: LabelState = { count: 0, className: "" };
// This is the state 'props' that are passed into each button component
upButtonState: ButtonState = {
buttonText: "Up",
className: "up",
buttonClickHandler: () => {
//reference to the internal method in the Label class
this.labelView!.model.increment();
},
};
downButtonState: ButtonState = {
buttonText: "Down",
className: "down",
buttonClickHandler: () => {
//reference to the internal method in the Label class
this.labelView!.model.decrement();
},
};
counterState
, upButtonState
, and downButtonState
are all objects that pass the default states into each component. Special note here is that as a part of the Button states, we are passing the event callback as state into the components, so we can re-use the components and control their behaviors. Since they are JS objects, and passed as reference, you can modify these values directly and it will change the values inside the component classes.
More information
More information can be found in the github repo for Peasy-Lib, you also will find all the other companion packages there too. Also, Peasy has a Discord server where we hang out and discuss Peasy and help each other out.
The author's twitter: Here
The author's itch: Here
Peasy-UI Github Repo: Here
Peasy Discord Server: Here
Conclusion
In summary, we looked at the different approaches that can be used to build Peasy-UI components, including single-file HTML components, or JS/TS based components. We looked at how you can use Peasy-UI components without a build step.
We also reviewed a demo application that demonstrates some of the component features and implementation details. This includes using both the Class and Instances of a component, and using different state objects to re-use a component with different behaviors.
We also walked through using a JS Class as the basis for creating the UIView for Peasy-UI that includes both the string template literal, and uses the class properties as the data model state.
The big take aways from a component based architecture is modular design, cleaner code, and being able to reuse common components to improve maintainability of your code base!
Top comments (0)