Skip to content

Simplified Web SPA app framework with vanilla JavaScript and CSS

License

Notifications You must be signed in to change notification settings

Xoboto/application.ts

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

12 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Application.Ts

A lightweight, dependency-free except template.ts and stackview.ts TypeScript framework for building single-page applications (SPAs) with pure vanilla JavaScript and CSS. No build tools required, just modern web standards.

πŸ“Ί Live Examples β†’

What is Application.Ts?

Application.Ts is a minimalist SPA framework that combines:

  • Routing: URL-based navigation with parameters and guards
  • Templating: Reactive data binding with Template.Ts
  • View Management: Stack-based view transitions with StackView.Ts
  • Component Model: Web Components for reusable UI elements

Built on web standards, Application.Ts provides a simple yet powerful foundation for creating modern web applications without the complexity of larger frameworks.

Quick Setup

npm install application.ts
import { App } from 'application.ts';
import { HomeView } from './views/home.view';

const app = new App('#root');

app.router
    .map('/', HomeView)
    .notFound(NotFoundView);

app.start();
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>My App</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/main.ts"></script>
  </body>
</html>

Features

πŸš€ Routing

  • Pattern-based routing with URL parameters
  • Navigation guards (canEnter)
  • Programmatic navigation
  • Browser history support
  • Query string handling
app.router
    .map('/', HomeView)
    .map('/user/:id', UserView)
    .map('/dashboard', DashboardView, {
        canEnter: () => AuthService.isLoggedIn()
    })
    .notFound(NotFoundView);

Route Guards with canEnter

Protect routes with navigation guards. The canEnter function can:

  • Return true to allow navigation
  • Return false to block navigation
  • Return a string path to redirect
// Simple authentication check
app.router.map('/dashboard', DashboardView, {
    canEnter: () => {
        return AuthService.isLoggedIn();
    }
});

// Redirect to login if not authenticated
app.router.map('/profile', ProfileView, {
    canEnter: () => {
        if (!AuthService.isLoggedIn()) {
            return '/login'; // Redirect to login page
        }
        return true; // Allow access
    }
});

// Access route parameters in guard
app.router.map('/admin/:section', AdminView, {
    canEnter: (params) => {
        if (!AuthService.isAdmin()) {
            return '/'; // Redirect to home
        }
        return true;
    }
});

// Async guards for API checks
app.router.map('/document/:id', DocumentView, {
    canEnter: async (params) => {
        const hasAccess = await checkDocumentPermission(params.id);
        return hasAccess ? true : '/unauthorized';
    }
});

🎨 Reactive Templates

Data binding with Template.Ts v2:

  • @on: - Event handlers
  • @prop: - Property binding
  • @att - Attribute binding
  • @batt - Boolean attribute binding
  • @if - Conditional rendering
  • @for - List rendering
  • {{ }} - Expression interpolation
const template = `
<div>
    <h1>{{ title }}</h1>
    <button @on:click="increment">Count: {{ count }}</button>
    <ul>
        <li @for="items">{{ item.name }}</li>
    </ul>
</div>`;

🧩 Component System

Build reusable components with AppView base class:

import { AppView, Register } from 'application.ts';

@Register
export class MyComponent extends AppView {
    template() {
        return `<div>{{ message }}</div>`;
    }

    state() {
        return { message: 'Hello World' };
    }
}

πŸ“ Layouts

Wrap views with shared layouts:

import { DefaultLayout } from './layouts/default.layout';

// Register and set default layout
app.registerLayout('default', DefaultLayout);
app.setDefaultLayout('default');

// Or specify layout per route
app.router
    .map('/', HomeView)
    .map('/about', AboutView, { meta: { layout: 'default' } });

🎯 View Lifecycle

Hook into view lifecycle events:

export class MyView extends AppView {
    async onMounted() {
        // View mounted to DOM
    }

    async stackViewShown() {
        // View became visible
    }

    async stackViewHidden() {
        // View hidden
    }
}

How to Use

1. Create a View

// views/home.view.ts
import { AppView, Register } from 'application.ts';

const template = `
<div class="home">
    <h1>{{ title }}</h1>
    <p>Counter: {{ count }}</p>
    <button @on:click="increment">Increment</button>
</div>`;

class State {
    title: string = 'Home Page';
    count: number = 0;
    
    increment: () => void = () => {
        this.count++;
    };
}

@Register
export class HomeView extends AppView {
    template() {
        return template;
    }

    state() {
        return new State();
    }
}

2. Set Up Routes

// main.ts
import { App } from 'application.ts';
import { HomeView } from './views/home.view';
import { AboutView } from './views/about.view';
import { UserView } from './views/user.view';

const app = new App('#root');

app.router
    .map('/', HomeView)
    .map('/about', AboutView)
    .map('/user/:id', UserView);

app.start();

3. Navigate Between Views

// Programmatic navigation
this.navigate('/about');
this.navigate('/user/123');

// In templates with links
<a href="/about">About</a>
<a href="/user/42">User Profile</a>

4. Access Route Parameters

export class UserView extends AppView {
    async onMounted() {
        const userId = this.params?.id;
        console.log('User ID:', userId);
    }
}

5. Create Components

// components/button.component.ts
import { AppView, Register } from 'application.ts';

const template = `
<button @on:click="handleClick" class="btn">
    {{ label }}
</button>`;

@Register
export class AppButton extends AppView {
    template() { return template; }
    
    state() {
        return {
            label: 'Click me',
            handleClick: () => {
                this.dispatchEvent(new CustomEvent('buttonclick', {
                    bubbles: true
                }));
            }
        };
    }
    
    get label() { return this.viewState.label; }
    set label(value: string) { this.setState('label', value); }
}

Use in templates:

<app-button @prop:label="'Save'" @on:buttonclick="handleSave"></app-button>

Examples

πŸš€ View Live Examples

Explore the /examples folder for complete working examples:

  • Minimal - The simplest possible app
  • Basic - Routing, layouts, and components
  • Advanced - Full-featured SPA with services, state management, and more

API Reference

App

const app = new App(selector: string, options?: AppOptions);
app.router // Access router
app.start() // Start the application

Router

router.map(path: string, view: typeof AppView, options?: RouteOptions)
router.notFound(view: typeof AppView)
router.navigate(path: string)
router.start()

App

app.registerLayout(handler: string, layoutClass: typeof AppView)
app.setDefaultLayout(handler: string)
app.registerView(handler: string, viewClass: typeof AppView)
app.start()

AppView

abstract class AppView {
    template(): string // Define HTML template
    state() // Define reactive state
    
    // Lifecycle hooks
    onBeforeMount()
    onMounted()
    onBeforeUnmount()
    onUnmounted()
    onStateChanged()
    onParamsChanged()
    
    // Navigation
    navigate(path: string)
    
    // State management
    setState(key: string, value: any)
    setStates(updates: Record<string, any>)
    update() // Force re-render
}

License

MIT

Contributing

Contributions are welcome! Please feel free to submit issues or pull requests.

Links