Getting started with TiniJS framework

RMAG news

Good weekend, friends! 😚

In the previous post, I have introduced you to a project that I’m currently working on – the TiniJS Framework, if you haven’t read it yet, I invite you to check it out – I’ve created yet another JavaScript framework.

Today we are going to explore the basic concepts of a TiniJS app, including project structure, dev/build tools and components.

To get started, you can download a starter template, or run npx @tinijs/cli@latest new my-app -l, or try the example apps on Stackblitz:

Photo Gallery App: https://stackblitz.com/edit/try-tinijs

To Do App: https://stackblitz.com/edit/try-tinijs-todo-app

Project structure

For a quick note about the term Projects in the TiniJS platform. Since the TiniJS platform is designed to be as versatile as possible, which means it is able to work with many favorite tools and other frameworks or no frameworks. Therefore, a TiniJS project could be literally any project as long as it involves one or more TiniJS aspects. For example: a Vue app using Tini UI, a React app using Tini Content, a project using Tini CLI expansion, … More about interoperable will be discussed along the way with future articles.

For this article, we will focus on projects involving an app built using TiniJS core framework. That’s being said, let explore TiniJS apps.

A TiniJS app may have any folder structure as you see fit for what you are comfortable to work with. But as a convention, I recommend you use the below structure for most of the case. At the very basic, an app must have two files:

Item
Description

app/index.html

The entry point of the single page app, where you define: title, meta tags, includes fonts, init app root, …

app/app.ts

The app-root element is where you create a TiniJS client app, register config, setup router, setup UI, …

As your app grows, we will add different types of code, there are places for different things inside a TiniJS app, we can organize them into these files and folders:

Item
Description

tini.config.ts
The main configuration source for various purposes across the TiniJS platform.

app/routes.ts

For Tini Router, where you define routing behavior of the app.

app/providers.ts

Working with services, utils, … depend on the pattern, you may choose to provide dependencies at the app level, then lazy load them later and inject to pages and components.

app/assets

For static assets such as images, SVG icons, …

app/public

For assets which will be copied as is upon build time and can be accessed from public URLs.

app/types

Shared Typescript types.

app/configs

Client app configuration files based on environments: development.ts, qa.ts, stage.ts, production.ts, … when using with the Default Compiler, depend on the target environment, a specific config file will be applied.

app/components

Reusable app components implement the TiniComponent class.

app/pages

App pages for routing purpose.

app/layouts

Layouts for pages.

app/partials

Small re-usable html templates which can be included in components and pages.

app/utils

Any type of shareable logic functions, depend on the pattern, you can either import or inject them.

app/services

Groups of similar utilities, you can either import or inject them.

app/consts

Shared constants.

app/classes

Constructors which are intended to be used to construct objects.

app/stores

Stores for global states management.

app/contexts

Consumable contexts for mitigating prop-drilling.

When integrate with other tools and frameworks, the specific integrated code will live in its own folder at the same level as the app folder.

Dev and build tools

The development and build workflow of TiniJS are backed by any of your favorite tools. You can either set them up manually or automatically using Tini CLI and official builders. Some of the tools currently available are: Vite, Parcel and Webpack.

In theory, you can use any other tools (Turbo Pack, Gulp, …). But I don’t have time to try them, it is opened up for community contribution.

Vite (recommended, default)

Homepage: https://vitejs.dev/

Option 1: Via Tini CLI

Install: npm i -D @tinijs/cli @tinijs/vite-builder

Add scripts:

dev: tini dev

build: tini build

Option 2: Or, manually

Install: npm i -D vite

Add scripts:

dev: vite app

build: vite build app –outDir www

Parcel

Homepage: https://parceljs.org/

Option 1: Via Tini CLI

Install: npm i -D @tinijs/cli @tinijs/parcel-builder

Config tini.config.ts, set build.builder to @tinijs/parcel-builder

Add scripts:

dev: tini dev

build: tini build

Option 2: Or, manually

Install: npm i -D parcel @parcel/config-default

Add scripts:

dev: parcel app/index.html –dist-dir www

build: parcel build app/index.html –dist-dir www

Additional setup

Either using Tini CLI or setup manually, you need to do these additional setup.

Modify package.json

{
“browserslist”: “> 0.5%, last 2 versions, not dead”,
“@parcel/resolver-default”: {
“packageExports”: true
}
}

Webpack

Homepage: https://webpack.js.org/

Option 1: Via Tini CLI

Install: npm i -D @tinijs/cli @tinijs/webpack-builder

Config tini.config.ts, set build.builder to @tinijs/webpack-builder

Add scripts:

dev: tini dev

build: tini build

Option 2: Or, manually

Install: npm i -D webpack webpack-cli webpack-dev-server html-bundler-webpack-plugin ts-loader

Add webpack.config.cjs, please see example.
Add scripts:

dev: webpack serve –history-api-fallback –mode development

build: webpack build –mode production

Additional setup

Either using Tini CLI or setup manually, you need to do these additional setup.

Modify tsconfig.json

{
“compilerOptions”: {
“declaration”: false
}
}

Working with Components

Components are basic building blocks of TiniJS apps. They are custom elements which extend the standard HTMLElement – the base class of native HTML elements. Since TiniJS is based on Lit, so it is nice to know how to define a component using LitElement, but it is not required because we will explore the basic concepts together.

Create components

To quickly scaffold a component using Tini CLI, run npx tini generate component <name>, a component file will be created at app/components/<name>.ts.

A TiniJS component looks like this:

import {html, css} from lit;
import {Component, TiniComponent} from @tinijs/core;

@Component()
export class AppXXXComponent extends TiniComponent {
static readonly defaultTagName = app-xxx;

// Logic here

protected render() {
return html`<p>Template here</p>`;
}

static styles = css`/* Style here */`;
}

There are 3 main sections:

Logic: class properties and methods for defining properties, internal states, events and other logic.

Template: HTML template with Lit html template literal syntax.

Style: CSS for styling the template.

Using components

To consume components, you must first register them, either globally or locally.

Register components globally at the app level, this is convenient since you only need to do it once per component, but it has the drawback that the initial bundle also includes all the related constructors.

// register components globally in app/app.ts

import {AppXXXComponent} from ./components/xxx.js;

@App({
components: [AppXXXComponent]
})
export class AppRoot extends TiniComponent {}

Components can also be registering locally at layout, app or component level. The benefit is that certain components will come with lazy-load pages instead of app initialization. The drawback is that it is repetitive (I think of auto import in the future, it may help a little).

// register components locally

import {AppXXXComponent} from ../components/xxx.js;

@Component|Page|Layout({
components: [AppXXXComponent]
})
export class ComponentOrPageOrLayout extends TiniComponent {}

Notice that there is defaultTagName = ‘…’. It is the default tag name of the component, you can register a component with a different tag name, use this syntax:

// AppFooComponent has the default tag name
static readonly defaultTagName = app-foo;

// register a different tag name
{
components: [
AppXXXComponent,
[AppFooComponent, bar-baz-qux]
]
}

After register, you can use the tag <app-xxx></app-xxx> and <bar-baz-qux></bar-baz-qux> as they are native HTML tags.

Props and events

Components usually has properties and events for data exchange and interaction.

Properties

Use the decorator @Input() or @property() to define properties.

import {property} from lit/decorators/property.js;
import {Input} from @tinijs/core;

@Component()
export class AppXXXComponent extends TiniComponent {

// Lit syntax
@property() prop1?: string;

// or, TiniJS syntax
@Input() prop2?: {foo: number};

}

Passing properties to components is similar to set attributes in native HTML elements, string values as in key=”value” and non-string as in .key=${varOrValue}.

html`<app-xxx prop1=“Lorem ipsum” .prop2=${{ foo: 999 }}></app-xxx>`

Beside define properties, you can also use Contexts as a form of communicating data. Sometime certain values are required by many components in a long-nested chain of components, passing values down the whole chain (aka. prop drilling) would be very annoying. Use contexts to provide and consume such values is more efficient. Please see more detail at https://lit.dev/docs/data/context/.

Events

Use the decorator @Output() to define events.

import {Output, type EventEmitter} from @tinijs/core;

@Component()
export class AppXXXComponent extends TiniComponent {
@Output() event1!: EventEmitter<string>;
@Output() event2!: EventEmitter<{ foo: number }>;

emitEvent1() {
this.event1.emit(Lorem ipsum);
}

protected render() {
return html`<button @click=${() => this.event2.emit({ foo: 999 })}></button>`;
}
}

Event payloads can be accessed via the detail field.

html`
<app-xxx
@event1=${({detail: event1Payload}: CustomEvent<string>) => console.log(event1Payload)}
@event2=${({detail: event2Payload}: CustomEvent<{ foo: number }>) => console.log(event2Payload)}
></app-xxx>
`

Handle states

By default, class properties changes won’t trigger the UI changes. In order to trigger render() every time a value changes you must implicitly define a state, states can either be local or global.

Local states

The properties defined using @property() or @Input() as we see above are already local states which means changing those properties will trigger render().

To define local states but not in the form of property, use the @state() or @Reactive().

import {state} from lit/decorators/state.js;
import {Reactive} from @tinijs/core;

@Component()
export class AppXXXComponent extends TiniComponent {

// Lit syntax
@state() state1?: string;

// or, TiniJS syntax
@Reactive() state2: number = 123;

protected render() {
return html`
<div>
${this.state1}</div>
<div>
${this.state2}</div>
`
;
}
}

Global states

States can also be organized in some central places (aka. stores). You can use Tini Store (very simple, ~50 lines) or other state management solutions such as MobX, TinyX, …

Getting started with Tini Store by install npm i @tinijs/store. Then create a store:

import {createStore} from @tinijs/store;

export const mainStore = createStore({
foo: bar
});

After creating a store, you now can access its states, subscribe to state changes and mutate states.

import {Subscribe} from @tinijs/store;

import {mainStore} from ./stores/main.js;

/*
* Access states
*/

const foo = mainStore.foo;

/*
* Mutate states
*/

// assign a new value
mainStore.foo = bar2;

// or, using the ‘commit’ method
mainStore.commit(foo, bar3);

/*
* Subscribe to state changes
*/

@Component()
export class AppXXXComponent extends TiniComponent {

// Use the @Subscribe() decorator
// this.foo will be updated when mainStore.foo changes it is reactive by default
@Subscribe(mainStore) foo = mainStore.foo;

// use a different variable name
@Subscribe(mainStore, foo) xyz = mainStore.foo;

// to turn of reactive, set the third argument to false
@Subscribe(mainStore, null, false) foo = mainStore.foo;

// Or, subscribe and unsubscribe manually
onInit() {
this.unsubscribeFoo = mainStore.subscribe(foo, value => {
// do something with the new value
});
}
// NOTE: remember to unsubscribe when the component is destroyed
onDestroy() {
this.unsubscribeFoo();
}

}

Use Signals

Another method for managing states is using Signals. Signals are an easy way to create shared observable state, state that many elements can use and update when it changes. Please see more detail at https://www.npmjs.com/package/@lit-labs/preact-signals.

Lifecyle hooks

Custom elements created by extending HTMLElement as well as LitElement have their lifecycle hooks for tapping into when needed, please see https://lit.dev/docs/components/lifecycle/ for more detail.

There are some other hooks supported by TiniComponent, includes: OnCreate, OnDestroy, OnChanges, OnFirstRender, OnRenders, OnInit, OnReady, OnChildrenRender, OnChildrenReady. Here is a quick summary of them.

OnCreate

Alias of connectedCallback() without the need of calling super.connectedCallback(). The very beginning of a component, the element has got connected to the DOM (more detail).

import {type OnCreate} from @tinijs/core;

export class Something extends TiniComponent implements OnCreate {
onCreate() {}
}

OnDestroy

Alias of disconnectedCallback() without the need of calling super.disconnectedCallback(). The end of an element, got removed from the DOM (more detail).

import {type OnDestroy} from @tinijs/core;

export class Something extends TiniComponent implements OnDestroy {
onDestroy() {}
}

OnChanges

Alias of willUpdate() of LitElement. Used to computed values using in the render() (more detail).

import {type OnChanges} from @tinijs/core;

export class Something extends TiniComponent implements OnChanges {
onChanges() {}
}

OnFirstRender

Alias of firstUpdated() of LitElement. The render() has run the first time (more detail).

import {type OnFirstRender} from @tinijs/core;

export class Something extends TiniComponent implements OnFirstRender {
onFirstRender() {}
}

OnRenders

Alias of updated() of LitElement. Changes has been updated and rendered (more detail).

import {type OnRenders} from @tinijs/core;

export class Something extends TiniComponent implements OnRenders {
onRenders() {}
}

OnInit

Can be used in interchangeable with OnCreate. When use with Lazy DI injection, injected dependencies available starting from onInit(), usually used to handle async tasks.

import {type OnInit} from @tinijs/core;

export class Something extends TiniComponent implements OnInit {
async onInit() {}
}

OnReady

Similar to OnFirstRender but it only counts after any async tasks in OnInit has been resolved and render (first stateful render).

import {type OnReady} from @tinijs/core;

export class Something extends TiniComponent implements OnReady {
async onReady() {}
}

Next topic will be: Working with pages, layouts, meta management and the Router.

Thank you for spending time with me. If there is anything not working for you, please contact me on Discord, I’m happy to assist.

Wish you all the best and happy coding! 💖

Leave a Reply

Your email address will not be published. Required fields are marked *