DEV Community

Cover image for Features, future-proof and interoperable in TiniJS apps
Nhan Lam
Nhan Lam

Posted on

Features, future-proof and interoperable in TiniJS apps

TiniJS adheres to the web standards! 🤝

With TiniJS Framework, beside the basic concepts such as Project Convention, App, Components, Pages, etc. and essential features such as Router, State Management, Back-end Solutions, etc. Our apps will definitely need more specific features for many different use cases.

Because TiniJS is based on the web standards, therefore in theory, anything that works in the browsers will work in TiniJS apps.

In this article, we will explore how to add features in a future-proof manner our TiniJS apps. To get started, you can download a starter template, or run:

npx @tinijs/cli@latest new my-app -t blank
Enter fullscreen mode Exit fullscreen mode

On Windows, please set { compile: false } in tini.config.ts before developement, there seems to a bug with the Default Compiler, will fix in the next release. 🙇‍♂️

The app instance

As in previous introduction guide of how to Get started with the TiniJS framework, we can see that a TiniJS app is started with an app root defined in ./app/app.ts, the file contains a class named AppRoot which extends the TiniComponent class and is decorated with the @App() decorator. The app root is where we initiate the app and incorporate features, a minimal app constructor looks like this:

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

@App()
export class AppRoot extends TiniComponent {
  protected render() {
    return html`<h1>Hello, world!</h1>`;
  }

  static styles = css``;
}
Enter fullscreen mode Exit fullscreen mode

From anywhere across our app, we can access the app instance using one of the following methods.

  • Using DOM methods:
// app-root is the only elem in the body
const app = document.body.firstElementChild;

// custom app-root placement
// <app-root id="xxx"></app-root>
const app = document.getElementById('xxx');
Enter fullscreen mode Exit fullscreen mode
  • Using util and decorator:
import {getApp, UseApp} from '@tinijs/core';

import type {AppRoot} from '../app.ts';

/*
 * Via util
 */
const app = getApp();
const router = app.router;
const meta = app.meta;
// any other hosted by the app root

/*
 * Or, via decorator
 */
@Page({})
export class AppPageXXX extends TiniComponent {

  @UseApp() app!: AppRoot;

  onCreate() {
    const config = this.app.config;
    const ui = this.app.ui;
    // any other hosted by the app root
  }

}
Enter fullscreen mode Exit fullscreen mode

Depend on the patterns you choose to add features to an app, the AppRoot plays a role as a host for defining such features (pattern 2 and 3) as we will see in the next section.

Add future-proof features

Features or dependencies in TiniJS apps are available in common forms:

  • Constants - any value, live in app/consts folder
  • Utilities - single responsible functions (in app/utils)
  • Services - groups of related utils (in app/services)

Those features are not limited to be used with TiniJS Framework, once written it can be use with any other frameworks or no framework.

But when it comes to work with dependencies in TiniJS apps, there are 3 main patterns to be considered, we can use one or all of them in our apps.

Pattern 1: Imports

Just simply import the utils and services directly to your components, pages. This is the common and most convenient way to work with utils and services.

You can use the generate command of Tini CLI to quickly scaffold consts, utils and services, run npx tini generate const|util|service <name>.

  • Step 1: Define consts, utils and services (in one or more files):
// define consts in app/consts folder
export const FOO = 'foo';

// define utils in app/utils folder
export function foo() {
  return 'foo';
}

// define services in app/services folder
class FooService {
  foo() {
    return 'foo';
  }
}
export const fooService = new FooService();
Enter fullscreen mode Exit fullscreen mode
  • Step 2: Use consts, utils and services:
// in pages, components, ...
import {FOO} from '../consts/foo.js';
import {foo} from '../utils/foo.js';
import {fooService} from '../services/foo.js';
Enter fullscreen mode Exit fullscreen mode

Pattern 2: Provide/Inject

TiniJS also provides a dependency injection mechanism (called Lazy DI) that allows you to lazy load and inject utils and services to your components, pages.

  • Step 1: Define dependencies (preferably in their own files):
// consts
export const FOO = 'foo';
export default FOO;
export type FooConst = typeof FOO;

// utils
export function foo() {}
export default foo;
export type FooUtil = typeof foo;

// services
export class FooService {}
export default FooService;
Enter fullscreen mode Exit fullscreen mode
  • Step 2: Provide dependencies in app/app.ts:
import type {DependencyProviders} from '@tinijs/core';

export const providers: DependencyProviders = {
  'FOO': () => import('./consts/foo.js'),
  'foo': () => import('./utils/foo.js'),
  'fooService': () => import('./services/foo.js'),
  // services depend on other dependencies or values
  'otherService': {
    provider: () => import('./services/other.js'),
    deps: [
      'fooService', // a string means using dependencies
      () => 'value', // a function means using values
    ],
  }
};

@App({ providers })
export class AppRoot extends TiniComponent {}
Enter fullscreen mode Exit fullscreen mode
  • Step 3: Inject dependencies:
import {Inject} from '@tinijs/core';

import type {FooConst} from '../consts/foo.js';
import type {FooUtil} from '../utils/foo.js';
import type {FooService} from '../services/foo.js';

@Page({})
export class AppPageXXX extends TiniComponent {

  @Inject() FOO!: FooConst;
  @Inject() foo!: FooUtil;
  @Inject() fooService!: FooService;

  // lazy load dependencies are available started from onInit() lifecycle hook
  onInit() {
    // this.FOO
    // this.foo()
    // this.fooService.foo()
  }

}
Enter fullscreen mode Exit fullscreen mode

Pattern 3: Plugins via Contexts

Values, utils and services can be provided and consumed using contexts.

Install Lit Context: npm i @lit/context, for usage details please visit https://lit.dev/docs/data/context/.

You can use the generate command of Tini CLI to quickly scaffold contexts, run npx tini generate context <name>.

  • Step 1: Define plugins in app/contexts:
import {createContext} from '@lit/context';

export type PluginsContext = typeof plugins;

export const pluginsContext = createContext<PluginsContext>(
  Symbol('plugins-context')
);

export const plugins = {
  FOO: 'foo',
  foo: () => 'foo',
  fooService: new FooService(),
};
Enter fullscreen mode Exit fullscreen mode

You can also split plugins into their own contexts, so that you can consume them individually.

  • Step 2: Register plugins in app/app.ts:
import {provide} from '@lit/context';

import {pluginsContext, plugins} from './contexts/plugins.ts';

@App()
export class AppRoot extends TiniComponent {

  @provide({context: pluginsContext}) $plugins = plugins;

}
Enter fullscreen mode Exit fullscreen mode
  • Step 3: Use plugins:
import {consume} from '@lit/context';

import {pluginsContext, type PluginsContext} from '../contexts/plugins.ts';

@Page({})
export class AppPageXXX extends TiniComponent {

  @consume({context: pluginsContext}) $plugins!: PluginsContext;

  onCreate() {
   // this.$plugins.FOO
   // this.$plugins.foo()
   // this.$plugins.fooService.foo()
  }

}
Enter fullscreen mode Exit fullscreen mode

Interoperable with other frameworks

Let admits the undeniable fact that we the JavaScript users are very proud about our framework of choice, there is even a war between developers to decide which framework is the best. Imagine telling a React developer to include the Vue or Angular runtime in their apps, it is outraged for sure.

Stop waging wars, be excellent to each other. 🩷

The TiniJS Framework is different, it is the first of its kind, which is designed to be used in conjunction with other frameworks as well, it means we may use other frameworks in TiniJS apps and in the other hand using TiniJS features in apps built using other frameworks. Interoperable is a comprehensive topic, we will explore more along the way with future articles. But, below is a basic introduction about the aspect.

For demonstration, we will use Vue 3 in a TiniJS app, install npm i vue.

import {ref, createRef, type Ref} from 'lit/directives/ref.js';
import {createApp} from 'vue/dist/vue.esm-bundler.js';

@Page({})
export class AppPageXXX extends TiniComponent {

  private readonly vueAppRef: Ref<HTMLDivElement> = createRef();

  onFirstRender() {
    const vueApp = this.vueAppRef.value!;
    createApp({
      template: `
        <div>
          <h1>Hello Vue 3!</h1>
          <p>Nice to have you here.</p>
        </div>
      `
    }).mount(vueApp);
  }

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

}

Enter fullscreen mode Exit fullscreen mode

For more features and stuffs, please visit https://tinijs.dev.


Thank you for spending time with me. If there was anything not working for you, please leave a comment or open an issue or ask for help on Discord, I'm happy to assist.

Wish you all the best and happy coding! 💖

Top comments (0)