Getting started with TiniJS framework
Tags: javascript, lit, webcomponents, webdev
Published: 2024-04-20 07:20:26

> **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](https://dev.to/lamnhan/ive-created-yet-another-javascript-framework-5c5o).

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](https://github.com/tinijs/bare-starter), or run `npx @tinijs/cli@latest new my-app`, or try the example apps on Stackblitz:
1. **Photo Gallery** App: [https://stackblitz.com/edit/try-tinijs](https://stackblitz.com/edit/try-tinijs?file=app%2Fapp.ts)
2. **To Do** App: [https://stackblitz.com/edit/try-tinijs-todo-app](https://stackblitz.com/edit/try-tinijs-todo-app?file=app%2Fapp.ts)

## 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](https://github.com/tinijs/tinijs/tree/main/packages/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](https://tinijs.dev/framework/compile), 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/`icons`   | Reusable icon components.                                                                                                                                                                                     |
| app/`partials`     | Small re-usable [html](https://lit.dev/docs/api/templates/#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 related 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](https://tinijs.dev/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 (recommended)

1. Install: `npm i -D @tinijs/cli @tinijs/vite-builder`
2. Add scripts:
  - **dev**: `tini dev`
  - **build**: `tini build`

#### Option 2: Or, manually

1. Install: `npm i -D vite`
2. Add scripts:
  - **dev**: `vite app`
  - **build**: `vite build app --outDir www`

### Parcel

Homepage: <https://parceljs.org/>

#### Option 1: Via Tini CLI (recommended)

1. Install: `npm i -D @tinijs/cli @tinijs/parcel-builder`
2. Config _tini.config.ts_, set **build.builder** to `@tinijs/parcel-builder`
3. Add scripts:
  - **dev**: `tini dev`
  - **build**: `tini build`

#### Option 2: Or, manually

1. Install: `npm i -D parcel @parcel/config-default`
2. 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_
```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 (recommended)

1. Install: `npm i -D @tinijs/cli @tinijs/webpack-builder`
2. Config _tini.config.ts_, set **build.builder** to `@tinijs/webpack-builder`
3. Add scripts:
  - **dev**: `tini dev`
  - **build**: `tini build`

#### Option 2: Or, manually

1. Install: `npm i -D webpack webpack-cli webpack-dev-server html-bundler-webpack-plugin ts-loader`
2. Add _webpack.config.cjs_, please see [example](https://github.com/tinijs/tinijs/blob/main/packages/webpack-builder/webpack.config.cjs).
3. Add scripts:
  - **dev**: `webpack serve --history-api-fallback --mode development`
  - **build**: `webpack build --mode production`

## Working with Components

Components are basic building blocks of TiniJS apps. They are [custom elements](https://developer.mozilla.org/en-US/docs/Web/API/Web_components) 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](https://lit.dev/docs/components/overview/), but it is not required because we will explore the basic concepts together.

### Create components

To quickly scaffold a component using [Tini CLI](https://tinijs.dev/cli), run `npx tini generate component <name>`, a component file will be created at `app/components/<name>.ts`.

**A TiniJS component** looks like this:

```ts
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](https://lit.dev/docs/api/templates/#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.

```ts
// 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).

```ts
// 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:

```ts
// 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>` just like 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.

```ts
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
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.

```ts
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
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()`.

```ts
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](https://github.com/tinijs/tinijs/tree/main/packages/store) (_very simple, ~50 lines_) or other state management solutions such as [MobX](https://mobx.js.org/), [TinyX](https://github.com/dmaevsky/tinyx), ...

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

```ts
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**.

```ts
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](https://lit.dev/docs/components/lifecycle/#connectedcallback)).

```ts
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](https://lit.dev/docs/components/lifecycle/#disconnectedcallback)). 

```ts
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](https://lit.dev/docs/components/lifecycle/#willupdate)).

```ts
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](https://lit.dev/docs/components/lifecycle/#firstupdated)).

```ts
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](https://lit.dev/docs/components/lifecycle/#updated)).

```ts
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.

```ts
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).

```ts
import {type OnReady} from '@tinijs/core';

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

---

Next topic: [**Working with pages, layouts, meta management and the Router**](https://dev.to/lamnhan/router-pages-layouts-and-async-data-in-tinijs-apps-3899).

Thank you for spending time with me. If there was anything not working for you, please leave a comment or [open an issue](https://github.com/tinijs/tinijs/issues/new) or ask for help on [Discord](https://discord.gg/EABbZVbPAb), I'm happy to assist.

**Wish you all the best and happy coding!** 💖