publish elexis v0.1
- rename: $AsyncNode -> $Async - remove Router - update README.md
This commit is contained in:
parent
d28953d354
commit
74327f8eed
43
$index.ts
43
$index.ts
@ -8,7 +8,6 @@ import { $Input } from "./lib/node/$Input";
|
|||||||
import { $Container } from "./lib/node/$Container";
|
import { $Container } from "./lib/node/$Container";
|
||||||
import { $Element } from "./lib/node/$Element";
|
import { $Element } from "./lib/node/$Element";
|
||||||
import { $Label } from "./lib/node/$Label";
|
import { $Label } from "./lib/node/$Label";
|
||||||
import { Router } from "./lib/router/Router";
|
|
||||||
import { $Image } from "./lib/node/$Image";
|
import { $Image } from "./lib/node/$Image";
|
||||||
import { $Canvas } from "./lib/node/$Canvas";
|
import { $Canvas } from "./lib/node/$Canvas";
|
||||||
import { $Dialog } from "./lib/node/$Dialog";
|
import { $Dialog } from "./lib/node/$Dialog";
|
||||||
@ -19,7 +18,7 @@ import { $OptGroup } from "./lib/node/$OptGroup";
|
|||||||
import { $Textarea } from "./lib/node/$Textarea";
|
import { $Textarea } from "./lib/node/$Textarea";
|
||||||
import { $Util } from "./lib/$Util";
|
import { $Util } from "./lib/$Util";
|
||||||
import { $HTMLElement } from "./lib/node/$HTMLElement";
|
import { $HTMLElement } from "./lib/node/$HTMLElement";
|
||||||
import { $AsyncNode } from "./lib/node/$AsyncNode";
|
import { $Async } from "./lib/node/$Async";
|
||||||
|
|
||||||
export type $ = typeof $;
|
export type $ = typeof $;
|
||||||
export function $<E extends $Element = $Element>(query: `::${string}`): E[];
|
export function $<E extends $Element = $Element>(query: `::${string}`): E[];
|
||||||
@ -41,25 +40,11 @@ export function $(resolver: any) {
|
|||||||
if (resolver.startsWith('::')) return Array.from(document.querySelectorAll(resolver.replace(/^::/, ''))).map(dom => $(dom));
|
if (resolver.startsWith('::')) return Array.from(document.querySelectorAll(resolver.replace(/^::/, ''))).map(dom => $(dom));
|
||||||
else if (resolver.startsWith(':')) return $(document.querySelector(resolver.replace(/^:/, '')));
|
else if (resolver.startsWith(':')) return $(document.querySelector(resolver.replace(/^:/, '')));
|
||||||
else if (resolver in $.TagNameElementMap) {
|
else if (resolver in $.TagNameElementMap) {
|
||||||
const instance = $.TagNameElementMap[resolver as keyof typeof $.TagNameElementMap]
|
const instance = $.TagNameElementMap[resolver as keyof $.TagNameElementMap]
|
||||||
switch (instance) {
|
if (instance === $HTMLElement) return new $HTMLElement(resolver);
|
||||||
case $HTMLElement: return new $HTMLElement(resolver);
|
if (instance === $Container) return new $Container(resolver);
|
||||||
case $Anchor: return new $Anchor();
|
//@ts-expect-error
|
||||||
case $Container: return new $Container(resolver);
|
return new instance();
|
||||||
case $Input: return new $Input();
|
|
||||||
case $Label: return new $Label();
|
|
||||||
case $Form: return new $Form();
|
|
||||||
case $Button: return new $Button();
|
|
||||||
case $Image: return new $Image();
|
|
||||||
case $Canvas: return new $Canvas();
|
|
||||||
case $Dialog: return new $Dialog();
|
|
||||||
case $View: return new $View();
|
|
||||||
case $Select: return new $Select();
|
|
||||||
case $Option: return new $Option();
|
|
||||||
case $OptGroup: return new $OptGroup();
|
|
||||||
case $Textarea: return new $Textarea();
|
|
||||||
case $AsyncNode: return new $AsyncNode();
|
|
||||||
}
|
|
||||||
} else return new $Container(resolver);
|
} else return new $Container(resolver);
|
||||||
}
|
}
|
||||||
if (resolver instanceof Node) {
|
if (resolver instanceof Node) {
|
||||||
@ -71,7 +56,6 @@ export function $(resolver: any) {
|
|||||||
export namespace $ {
|
export namespace $ {
|
||||||
export let anchorHandler: null | (($a: $Anchor, e: Event) => void) = null;
|
export let anchorHandler: null | (($a: $Anchor, e: Event) => void) = null;
|
||||||
export let anchorPreventDefault: boolean = false;
|
export let anchorPreventDefault: boolean = false;
|
||||||
export const routers = new Set<Router>;
|
|
||||||
export const TagNameElementMap = {
|
export const TagNameElementMap = {
|
||||||
'document': $Document,
|
'document': $Document,
|
||||||
'body': $Container,
|
'body': $Container,
|
||||||
@ -104,10 +88,12 @@ export namespace $ {
|
|||||||
'option': $Option,
|
'option': $Option,
|
||||||
'optgroup': $OptGroup,
|
'optgroup': $OptGroup,
|
||||||
'textarea': $Textarea,
|
'textarea': $Textarea,
|
||||||
'async': $AsyncNode,
|
'async': $Async,
|
||||||
}
|
}
|
||||||
|
export type TagNameElementMapType = typeof TagNameElementMap;
|
||||||
|
export interface TagNameElementMap extends TagNameElementMapType {}
|
||||||
export type TagNameTypeMap = {
|
export type TagNameTypeMap = {
|
||||||
[key in keyof typeof $.TagNameElementMap]: InstanceType<typeof $.TagNameElementMap[key]>;
|
[key in keyof $.TagNameElementMap]: InstanceType<$.TagNameElementMap[key]>;
|
||||||
};
|
};
|
||||||
export type ContainerTypeTagName = Exclude<keyof TagNameTypeMap, 'input'>;
|
export type ContainerTypeTagName = Exclude<keyof TagNameTypeMap, 'input'>;
|
||||||
export type SelfTypeTagName = 'input';
|
export type SelfTypeTagName = 'input';
|
||||||
@ -128,10 +114,6 @@ export namespace $ {
|
|||||||
: H extends HTMLTextAreaElement ? $Textarea
|
: H extends HTMLTextAreaElement ? $Textarea
|
||||||
: $Container<H>;
|
: $Container<H>;
|
||||||
|
|
||||||
export function open(path: string | URL | undefined) { return Router.open(path) }
|
|
||||||
export function replace(path: string | URL | undefined) { return Router.replace(path) }
|
|
||||||
export function back() { return Router.back() }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A helper for fluent method design. Return the `instance` object when arguments length not equal 0. Otherwise, return the `value`.
|
* A helper for fluent method design. Return the `instance` object when arguments length not equal 0. Otherwise, return the `value`.
|
||||||
* @param instance The object to return when arguments length not equal 0.
|
* @param instance The object to return when arguments length not equal 0.
|
||||||
@ -249,6 +231,11 @@ export namespace $ {
|
|||||||
else return false;
|
else return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function registerTagName(string: string, node: {new(...args: undefined[]): $Node}) {
|
||||||
|
Object.assign($.TagNameElementMap, {[string]: node});
|
||||||
|
return $.TagNameElementMap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
type BuildNodeFunction = (...args: any[]) => $Node;
|
type BuildNodeFunction = (...args: any[]) => $Node;
|
||||||
type BuilderSelfFunction<K extends $Node> = (self: K) => void;
|
type BuilderSelfFunction<K extends $Node> = (self: K) => void;
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,4 +1,3 @@
|
|||||||
extensions
|
|
||||||
.npmignore
|
.npmignore
|
||||||
bun.lockb
|
bun.lockb
|
||||||
node_modules
|
node_modules
|
158
README.md
158
README.md
@ -1,132 +1,44 @@
|
|||||||
# fluentX - Fast, fluent, simple web builder.
|
# ElexisJS
|
||||||
Inspired by jQuery, but not selecting query anymore, just create it.
|
TypeScript First Web Framework, for Humans.
|
||||||
|
> ElexisJS is still in beta test now, some breaking changes might happen very often.
|
||||||
|
|
||||||
## Usage
|
## What does ElexisJS bring to developer?
|
||||||
|
1. Write website with Native JavaScript syntax and full TypeScript development experiance, no more HTML or JSX.
|
||||||
|
2. For fluent method lovers.
|
||||||
|
3. Easy to import or create extensions to extend more functional.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
1. Install from npm
|
||||||
|
```
|
||||||
|
npm i elexis
|
||||||
|
```
|
||||||
|
2. Import to your project main entry js/ts file.
|
||||||
|
```ts
|
||||||
|
import 'elexis';
|
||||||
|
```
|
||||||
|
3. Use web packaging tools like [Vite](https://vitejs.dev/) to compile your project.
|
||||||
|
|
||||||
|
## How to Create Element
|
||||||
|
Using the simple $ function to create any element with node name.
|
||||||
```ts
|
```ts
|
||||||
import { $ } from 'fluentx'
|
$('a');
|
||||||
|
```
|
||||||
|
> This is not jQuery selector! It looks like same but it actually create `<a>` element, not selecting them.
|
||||||
|
|
||||||
const $app = $('app').content([
|
## Fluent method
|
||||||
$('h1').content('Hello World!')
|
Create and modify element in one line.
|
||||||
])
|
```ts
|
||||||
|
$('h1').class('title').css({color: 'red'})
|
||||||
document.body.append($app.dom) // render $app
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Forget HTML, create any element just like this
|
## Build your first "Hello, world!" ElexisJS project
|
||||||
|
Let's try this in your entry file:
|
||||||
```ts
|
```ts
|
||||||
$('a')
|
$(document.body).content([
|
||||||
```
|
$('h1').class('title').content('Hello, world!')
|
||||||
|
|
||||||
## Yes, Fluent Method.
|
|
||||||
```ts
|
|
||||||
$('h1').class('amazing-title').css({color: 'red'}).content('Fluuuuuuuuuuuuent!')
|
|
||||||
```
|
|
||||||
|
|
||||||
## Single Page App with Router
|
|
||||||
```ts
|
|
||||||
const router = new Router('/')
|
|
||||||
// example.com
|
|
||||||
.addRoute(new Route('/', () => 'Welcome to your first page!'))
|
|
||||||
|
|
||||||
// example.com/user/anyusername
|
|
||||||
.addRoute(new Route('/user/:username', (params) => {
|
|
||||||
return $('div').content([
|
|
||||||
$('h1').content(params.username)
|
|
||||||
])
|
|
||||||
}))
|
|
||||||
|
|
||||||
.listen() // start resolve pathname and listen state change
|
|
||||||
|
|
||||||
// prevent jump to other page from <a> link
|
|
||||||
$.anchorPreventDefault = true;
|
|
||||||
$.anchorHandler = (url) => { router.open(url) }
|
|
||||||
|
|
||||||
$('a').href('/about').content('Click me will not reload page.')
|
|
||||||
```
|
|
||||||
|
|
||||||
## Insert element(s) with condition
|
|
||||||
```ts
|
|
||||||
// Example 1
|
|
||||||
$('div').content([
|
|
||||||
$('h1').content(params.username),
|
|
||||||
// conditional
|
|
||||||
params.username === 'admin' ? $('span').content('Admin is here!') : undefined
|
|
||||||
])
|
|
||||||
|
|
||||||
// Example 2
|
|
||||||
$('div').content([
|
|
||||||
$('h1').content(params.username),
|
|
||||||
params.username === 'alien' ? [
|
|
||||||
// the elements in this array will insert to <div> when conditional is true
|
|
||||||
$('span').content('Warning'),
|
|
||||||
$('span').content('You are contacting with alien!')
|
|
||||||
] : undefined
|
|
||||||
])
|
])
|
||||||
```
|
```
|
||||||
|
|
||||||
## Replace or Insert
|
## Extensions
|
||||||
```ts
|
1. [@elexis/router](https://github.com/elexisjs/router): Router for Single Page App.
|
||||||
$('div').content(['1', '2', '3']) // 123
|
2. [@elexis/layout](https://github.com/elexisjs/layout): Build waterfall/justified layout with automatic compute content size and position.
|
||||||
.content(['4']) // 4
|
|
||||||
// content method will replace children with elements
|
|
||||||
|
|
||||||
.insert(['5', '6', '7']) // 4567
|
|
||||||
// using insert method to avoid replacement
|
|
||||||
|
|
||||||
.class('class1, class2') // class1, class2
|
|
||||||
// class method is replacement method
|
|
||||||
|
|
||||||
.addClass('class3') // class1, class2, class3
|
|
||||||
// using addClass method
|
|
||||||
```
|
|
||||||
|
|
||||||
## Multiple element builder
|
|
||||||
```ts
|
|
||||||
$('ul').content([
|
|
||||||
// create 10 <li> element with same content
|
|
||||||
$.builder('li', 10, ($li) => $li.content('Not a unique content of list item!'))
|
|
||||||
|
|
||||||
// create <li> element depend on array length
|
|
||||||
$.builder('li', [
|
|
||||||
// if insert a function,
|
|
||||||
// builder will callback this function after create this <li> element
|
|
||||||
($li) => $li.css({color: 'red'}).content('List item with customize style!'),
|
|
||||||
|
|
||||||
// if insert a string or element,
|
|
||||||
// builder will create <li> element and insert this into <li>
|
|
||||||
'List item with just text',
|
|
||||||
$('a').href('/').content('List item but with a link!')
|
|
||||||
])
|
|
||||||
])
|
|
||||||
```
|
|
||||||
|
|
||||||
## Element builder with function
|
|
||||||
```ts
|
|
||||||
// This is a template function that return a <div> element
|
|
||||||
function UserCard(name: string, age: number) {
|
|
||||||
return $('div').content([
|
|
||||||
$('h2').content(name),
|
|
||||||
$('span').content(`${bio} year old`)
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
// A user data array
|
|
||||||
const userDataList = [
|
|
||||||
{ name: 'Amateras', age: 16 },
|
|
||||||
{ name: 'Tsukimi', age: 16},
|
|
||||||
{ name: 'Rei', age: 14},
|
|
||||||
{ name: 'Ichi', age: 14},
|
|
||||||
]
|
|
||||||
|
|
||||||
// This function will create 10 UserCard element with same name and age
|
|
||||||
// Using tuple [Function, ...args] to call function with paramerters
|
|
||||||
$.builder([UserCard, 'Shizuka', 16], 100)
|
|
||||||
|
|
||||||
// This function will create UserCard with the amount depend on array length
|
|
||||||
$.builder(
|
|
||||||
UserCard,
|
|
||||||
userDataList.map(userData => [userData.name, userData.age]))
|
|
||||||
|
|
||||||
// Same result as (prefer)
|
|
||||||
userDataList.map(userData => UserCard(userData.name, userData.age))
|
|
||||||
```
|
|
6
index.ts
6
index.ts
@ -18,7 +18,7 @@ declare global {
|
|||||||
type TextDirection = 'ltr' | 'rtl' | 'auto' | '';
|
type TextDirection = 'ltr' | 'rtl' | 'auto' | '';
|
||||||
type ImageDecoding = "async" | "sync" | "auto";
|
type ImageDecoding = "async" | "sync" | "auto";
|
||||||
type ImageLoading = "eager" | "lazy";
|
type ImageLoading = "eager" | "lazy";
|
||||||
type ContructorType<T> = { new (...args: any[]): T }
|
type ConstructorType<T> = { new (...args: any[]): T }
|
||||||
interface Node {
|
interface Node {
|
||||||
$: import('./lib/node/$Node').$Node;
|
$: import('./lib/node/$Node').$Node;
|
||||||
}
|
}
|
||||||
@ -30,8 +30,6 @@ Array.prototype.detype = function <T extends undefined | null, O>(this: O[], ...
|
|||||||
}) as Exclude<O, T>[];
|
}) as Exclude<O, T>[];
|
||||||
}
|
}
|
||||||
export * from "./$index";
|
export * from "./$index";
|
||||||
export * from "./lib/router/Route";
|
|
||||||
export * from "./lib/router/Router";
|
|
||||||
export * from "./lib/node/$Node";
|
export * from "./lib/node/$Node";
|
||||||
export * from "./lib/node/$Anchor";
|
export * from "./lib/node/$Anchor";
|
||||||
export * from "./lib/node/$Element";
|
export * from "./lib/node/$Element";
|
||||||
@ -48,5 +46,5 @@ export * from "./lib/node/$Option";
|
|||||||
export * from "./lib/node/$OptGroup";
|
export * from "./lib/node/$OptGroup";
|
||||||
export * from "./lib/node/$Textarea";
|
export * from "./lib/node/$Textarea";
|
||||||
export * from "./lib/node/$Image";
|
export * from "./lib/node/$Image";
|
||||||
export * from "./lib/node/$AsyncNode";
|
export * from "./lib/node/$Async";
|
||||||
export * from "./lib/node/$Document";
|
export * from "./lib/node/$Document";
|
22
lib/node/$Async.ts
Normal file
22
lib/node/$Async.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { $Container, $ContainerOptions } from "./$Container";
|
||||||
|
import { $Node } from "./$Node";
|
||||||
|
export interface $AsyncNodeOptions extends $ContainerOptions {}
|
||||||
|
export class $Async<N extends $Node = $Node> extends $Container {
|
||||||
|
#loaded: boolean = false;
|
||||||
|
constructor(options?: $AsyncNodeOptions) {
|
||||||
|
super('async', options)
|
||||||
|
}
|
||||||
|
|
||||||
|
await<T extends $Node = $Node>($node: Promise<T>) {
|
||||||
|
$node.then($node => this._loaded($node));
|
||||||
|
return this as $Async<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _loaded($node: $Node) {
|
||||||
|
this.#loaded = true;
|
||||||
|
this.replace($node)
|
||||||
|
this.dom.dispatchEvent(new Event('load'))
|
||||||
|
}
|
||||||
|
|
||||||
|
get loaded() { return this.#loaded }
|
||||||
|
}
|
@ -1,23 +0,0 @@
|
|||||||
import { $Node } from "./$Node";
|
|
||||||
export interface $AsyncNodeOptions {
|
|
||||||
dom?: Node;
|
|
||||||
}
|
|
||||||
export class $AsyncNode<N extends $Node = $Node> extends $Node {
|
|
||||||
dom: Node = document.createElement('async');
|
|
||||||
loaded: boolean = false;
|
|
||||||
constructor(options?: $AsyncNodeOptions) {
|
|
||||||
super()
|
|
||||||
this.dom.$ = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
await<T extends $Node = $Node>($node: Promise<T>) {
|
|
||||||
$node.then($node => this._loaded($node));
|
|
||||||
return this as $AsyncNode<T>
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _loaded($node: $Node) {
|
|
||||||
this.loaded = true;
|
|
||||||
this.replace($node)
|
|
||||||
this.dom.dispatchEvent(new Event('load'))
|
|
||||||
}
|
|
||||||
}
|
|
@ -43,10 +43,10 @@ export class $Container<H extends HTMLElement = HTMLElement> extends $HTMLElemen
|
|||||||
}
|
}
|
||||||
|
|
||||||
//**Query selector one of child element */
|
//**Query selector one of child element */
|
||||||
$<E extends $Element>(query: string) { return $(this.dom.querySelector(query)) as E | null }
|
$<E extends $Element>(query: string): E | null { return $(this.dom.querySelector(query)) as E | null }
|
||||||
|
|
||||||
//**Query selector of child elements */
|
//**Query selector of child elements */
|
||||||
$all<E extends $Element>(query: string) { return Array.from(this.dom.querySelectorAll(query)).map($dom => $($dom) as E) }
|
$all<E extends $Element>(query: string): E[] { return Array.from(this.dom.querySelectorAll(query)).map($dom => $($dom) as E) }
|
||||||
|
|
||||||
get scrollHeight() { return this.dom.scrollHeight }
|
get scrollHeight() { return this.dom.scrollHeight }
|
||||||
get scrollWidth() { return this.dom.scrollWidth }
|
get scrollWidth() { return this.dom.scrollWidth }
|
||||||
|
@ -1,79 +0,0 @@
|
|||||||
# fluentX/router
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
```ts
|
|
||||||
import { $, Router, Route } from 'fluentX';
|
|
||||||
|
|
||||||
// create new Router with base path '/',
|
|
||||||
// also create a custom view Element for router container
|
|
||||||
const router = new Router('/', $('view'));
|
|
||||||
|
|
||||||
// append router view element
|
|
||||||
const $app = $('app').content([
|
|
||||||
router.$view
|
|
||||||
])
|
|
||||||
|
|
||||||
const home_page_route = new Route('/', () => {
|
|
||||||
// which this callback function return will be appended
|
|
||||||
// into router view element when path match
|
|
||||||
return $('h1').content('This is a Homepage!')
|
|
||||||
})
|
|
||||||
|
|
||||||
// Add home_page_route into router
|
|
||||||
router.addRoute(home_page_route);
|
|
||||||
// Router starting listen location path change
|
|
||||||
router.listen();
|
|
||||||
```
|
|
||||||
|
|
||||||
## Events
|
|
||||||
|
|
||||||
### Router - load
|
|
||||||
```ts
|
|
||||||
const gallery_route = new Route('/gallery', ({loaded}) => {
|
|
||||||
|
|
||||||
async function $ImageList() {
|
|
||||||
// fetch images and return elements
|
|
||||||
|
|
||||||
// after all image loaded, using loaded function.
|
|
||||||
// this will trigger load event on anccestor router
|
|
||||||
loaded();
|
|
||||||
}
|
|
||||||
return $('div').content([
|
|
||||||
$ImageList()
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
router.on('load', () => {...})
|
|
||||||
```
|
|
||||||
|
|
||||||
### RouteRecord - open
|
|
||||||
```ts
|
|
||||||
const viewer_route = new Route('/viewer', ({record}) => {
|
|
||||||
const page_open_count$ = $.state(0);
|
|
||||||
|
|
||||||
// this event will be fire everytime this page opened
|
|
||||||
record.on('open', () => {
|
|
||||||
page_open_count$.set( page_open_count$.value + 1 );
|
|
||||||
})
|
|
||||||
|
|
||||||
return $('div').content(page_open_count$)
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Router - notfound
|
|
||||||
```ts
|
|
||||||
// Route will remove all child of view when path is not exist.
|
|
||||||
// Using preventDefault function to prevent this action.
|
|
||||||
router.on('notfound', ({preventDefault}) => {
|
|
||||||
preventDefault(); // prevent remove all child of view
|
|
||||||
... // do something
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Router - pathchange
|
|
||||||
```ts
|
|
||||||
// This event fired on location change happened
|
|
||||||
router.on('pathchange', () => {
|
|
||||||
... // do something
|
|
||||||
})
|
|
||||||
```
|
|
@ -1,48 +0,0 @@
|
|||||||
import { $EventManager, $EventMethod } from "../$EventManager";
|
|
||||||
import { $Node } from "../node/$Node";
|
|
||||||
import { $Util } from "../$Util";
|
|
||||||
export class Route<Path extends string | PathResolverFn> {
|
|
||||||
path: string | PathResolverFn;
|
|
||||||
builder: (req: RouteRequest<Path>) => RouteContent;
|
|
||||||
constructor(path: Path, builder: ((req: RouteRequest<Path>) => RouteContent) | RouteContent) {
|
|
||||||
this.path = path;
|
|
||||||
this.builder = builder instanceof Function ? builder : (req: RouteRequest<Path>) => builder;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type PathParams<Path> = Path extends `${infer Segment}/${infer Rest}`
|
|
||||||
? Segment extends `${string}:${infer Param}` ? Record<Param, string> & PathParams<Rest> : PathParams<Rest>
|
|
||||||
: Path extends `${string}:${infer Param}` ? Record<Param,string> : {}
|
|
||||||
|
|
||||||
export type PathResolverFn = (path: string) => undefined | string;
|
|
||||||
|
|
||||||
type PathParamResolver<P extends PathResolverFn | string> = P extends PathResolverFn
|
|
||||||
? undefined : PathParams<P>
|
|
||||||
|
|
||||||
// type PathResolverRecord<P extends PathResolverFn> = {
|
|
||||||
// [key in keyof ReturnType<P>]: ReturnType<P>[key]
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
export interface RouteRecord extends $EventMethod<RouteRecordEventMap> {};
|
|
||||||
export class RouteRecord {
|
|
||||||
id: string;
|
|
||||||
readonly content?: $Node;
|
|
||||||
events = new $EventManager<RouteRecordEventMap>().register('open', 'load')
|
|
||||||
constructor(id: string) {
|
|
||||||
this.id = id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$Util.mixin(RouteRecord, $EventMethod)
|
|
||||||
export interface RouteRecordEventMap {
|
|
||||||
'open': [{path: string, record: RouteRecord}];
|
|
||||||
'load': [{path: string, record: RouteRecord}];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RouteRequest<Path extends PathResolverFn | string> {
|
|
||||||
params: PathParamResolver<Path>,
|
|
||||||
record: RouteRecord,
|
|
||||||
loaded: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RouteContent = $Node | string | void;
|
|
@ -1,192 +0,0 @@
|
|||||||
import { $EventManager, $EventMethod } from "../$EventManager";
|
|
||||||
import { $Text } from "../node/$Text";
|
|
||||||
import { $Util } from "../$Util";
|
|
||||||
import { $View } from "../node/$View";
|
|
||||||
import { PathResolverFn, Route, RouteRecord } from "./Route";
|
|
||||||
export interface Router extends $EventMethod<RouterEventMap> {};
|
|
||||||
export class Router {
|
|
||||||
routeMap = new Map<string | PathResolverFn, Route<any>>();
|
|
||||||
recordMap = new Map<string, RouteRecord>();
|
|
||||||
$view: $View;
|
|
||||||
static index: number = 0;
|
|
||||||
static events = new $EventManager<RouterGlobalEventMap>().register('pathchange', 'notfound', 'load');
|
|
||||||
events = new $EventManager<RouterEventMap>().register('notfound', 'load');
|
|
||||||
basePath: string;
|
|
||||||
static currentPath: URL = new URL(location.href);
|
|
||||||
constructor(basePath: string, view?: $View) {
|
|
||||||
this.basePath = basePath;
|
|
||||||
this.$view = view ?? new $View();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**Add route to Router. @example Router.addRoute(new Route('/', 'Hello World')) */
|
|
||||||
addRoute(routes: OrArray<Route<any>>) {
|
|
||||||
routes = $.orArrayResolve(routes);
|
|
||||||
for (const route of routes) this.routeMap.set(route.path, route);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**Start listen to the path change */
|
|
||||||
listen() {
|
|
||||||
if (!history.state || 'index' in history.state === false) {
|
|
||||||
const routeData: RouteData = {index: Router.index, data: {}}
|
|
||||||
history.replaceState(routeData, '')
|
|
||||||
} else {
|
|
||||||
Router.index = history.state.index
|
|
||||||
}
|
|
||||||
addEventListener('popstate', this.popstate)
|
|
||||||
$.routers.add(this);
|
|
||||||
this.resolvePath();
|
|
||||||
Router.events.fire('pathchange', {prevURL: undefined, nextURL: Router.currentPath, navigation: 'Forward'});
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**Open URL */
|
|
||||||
static open(url: string | URL | undefined) {
|
|
||||||
if (url === undefined) return this;
|
|
||||||
url = new URL(url);
|
|
||||||
if (url.origin !== location.origin) return this;
|
|
||||||
if (url.href === location.href) return this;
|
|
||||||
const prevPath = Router.currentPath;
|
|
||||||
this.index += 1;
|
|
||||||
const routeData: RouteData = { index: this.index, data: {} };
|
|
||||||
history.pushState(routeData, '', url);
|
|
||||||
Router.currentPath = new URL(location.href);
|
|
||||||
$.routers.forEach(router => router.resolvePath())
|
|
||||||
Router.events.fire('pathchange', {prevURL: prevPath, nextURL: Router.currentPath, navigation: 'Forward'});
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**Back to previous page */
|
|
||||||
static back() {
|
|
||||||
const prevPath = Router.currentPath;
|
|
||||||
history.back();
|
|
||||||
Router.currentPath = new URL(location.href);
|
|
||||||
Router.events.fire('pathchange', {prevURL: prevPath, nextURL: Router.currentPath, navigation: 'Back'});
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
static replace(url: string | URL | undefined) {
|
|
||||||
if (url === undefined) return this;
|
|
||||||
if (typeof url === 'string' && !url.startsWith(location.origin)) url = location.origin + url;
|
|
||||||
url = new URL(url);
|
|
||||||
if (url.origin !== location.origin) return this;
|
|
||||||
if (url.href === location.href) return this;
|
|
||||||
const prevPath = Router.currentPath;
|
|
||||||
history.replaceState({index: Router.index}, '', url)
|
|
||||||
Router.currentPath = new URL(location.href);
|
|
||||||
$.routers.forEach(router => router.resolvePath(url.pathname));
|
|
||||||
Router.events.fire('pathchange', {prevURL: prevPath, nextURL: Router.currentPath, navigation: 'Forward'});
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
setStateData(key: string, value: any) {
|
|
||||||
if (history.state.data === undefined) history.state.data = {};
|
|
||||||
history.state.data[key] = value;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
private popstate = (() => {
|
|
||||||
// Forward
|
|
||||||
if (history.state.index > Router.index) { }
|
|
||||||
// Back
|
|
||||||
else if (history.state.index < Router.index) { }
|
|
||||||
const prevPath = Router.currentPath;
|
|
||||||
Router.index = history.state.index;
|
|
||||||
this.resolvePath();
|
|
||||||
Router.currentPath = new URL(location.href);
|
|
||||||
Router.events.fire('pathchange', {prevURL: prevPath, nextURL: Router.currentPath, navigation: 'Forward'});
|
|
||||||
}).bind(this)
|
|
||||||
|
|
||||||
private resolvePath(path = location.pathname) {
|
|
||||||
if (!path.startsWith(this.basePath)) return;
|
|
||||||
path = path.replace(this.basePath, '/').replace('//', '/');
|
|
||||||
let found = false;
|
|
||||||
const openCached = (pathId: string) => {
|
|
||||||
const record = this.recordMap.get(pathId);
|
|
||||||
if (record) {
|
|
||||||
found = true;
|
|
||||||
if (record.content && !this.$view.contains(record.content)) this.$view.switchView(pathId);
|
|
||||||
record.events.fire('open', {path, record});
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const create = (pathId: string, route: Route<any>, data: any) => {
|
|
||||||
const record = new RouteRecord(pathId);
|
|
||||||
let content = route.builder({
|
|
||||||
params: data,
|
|
||||||
record: record,
|
|
||||||
loaded: () => {
|
|
||||||
record.events.fire('load', {path: pathId, record});
|
|
||||||
this.events.fire('load', {path: pathId});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (typeof content === 'string') content = new $Text(content);
|
|
||||||
if (content === undefined) return;
|
|
||||||
(record as Mutable<RouteRecord>).content = content;
|
|
||||||
this.recordMap.set(pathId, record);
|
|
||||||
this.$view.setView(pathId, content).switchView(pathId);
|
|
||||||
record.events.fire('open', {path, record});
|
|
||||||
found = true;
|
|
||||||
}
|
|
||||||
for (const [pathResolver, route] of this.routeMap.entries()) {
|
|
||||||
// PathResolverFn
|
|
||||||
if (pathResolver instanceof Function) {
|
|
||||||
const routeId = pathResolver(path)
|
|
||||||
if (routeId) { if (!openCached(routeId)) create(routeId, route, undefined) }
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// string
|
|
||||||
const [_routeParts, _pathParts] = [pathResolver.split('/').map(p => `/${p}`), path.split('/').map(p => `/${p}`)];
|
|
||||||
_routeParts.shift(); _pathParts.shift();
|
|
||||||
const data = {};
|
|
||||||
let pathString = '';
|
|
||||||
for (let i = 0; i < _pathParts.length; i++) {
|
|
||||||
const [routePart, pathPart] = [_routeParts.at(i), _pathParts.at(i)];
|
|
||||||
if (!routePart || !pathPart) continue;
|
|
||||||
if (routePart === pathPart) {
|
|
||||||
pathString += pathPart;
|
|
||||||
if (routePart === _routeParts.at(-1)) {
|
|
||||||
if (!openCached(pathString)) create(pathString, route, data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (routePart.includes(':')) {
|
|
||||||
const [prefix, param] = routePart.split(':');
|
|
||||||
if (!pathPart.startsWith(prefix)) continue;
|
|
||||||
Object.assign(data, {[param]: pathPart.replace(prefix, '')})
|
|
||||||
pathString += pathPart;
|
|
||||||
if (routePart === _routeParts.at(-1)) {
|
|
||||||
if (!openCached(pathString)) create(pathString, route, data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!found) {
|
|
||||||
let preventDefaultState = false;
|
|
||||||
const preventDefault = () => preventDefaultState = true;
|
|
||||||
this.events.fire('notfound', {path, preventDefault});
|
|
||||||
if (!preventDefaultState) this.$view.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static on<K extends keyof RouterGlobalEventMap>(type: K, callback: (...args: RouterGlobalEventMap[K]) => any) { this.events.on(type, callback); return this }
|
|
||||||
static off<K extends keyof RouterGlobalEventMap>(type: K, callback: (...args: RouterGlobalEventMap[K]) => any) { this.events.off(type, callback); return this }
|
|
||||||
static once<K extends keyof RouterGlobalEventMap>(type: K, callback: (...args: RouterGlobalEventMap[K]) => any) { this.events.once(type, callback); return this }
|
|
||||||
}
|
|
||||||
$Util.mixin(Router, $EventMethod);
|
|
||||||
interface RouterEventMap {
|
|
||||||
notfound: [{path: string, preventDefault: () => any}];
|
|
||||||
load: [{path: string}];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RouterGlobalEventMap {
|
|
||||||
pathchange: [{prevURL?: URL, nextURL: URL, navigation: 'Back' | 'Forward'}];
|
|
||||||
}
|
|
||||||
|
|
||||||
type RouteData = {
|
|
||||||
index: number;
|
|
||||||
data: {[key: string]: any};
|
|
||||||
}
|
|
17
package.json
17
package.json
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "fluentx",
|
"name": "elexis",
|
||||||
"version": "0.0.9",
|
"description": "Web library design for JS/TS lover.",
|
||||||
|
"version": "0.1.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "defaultkavy",
|
"name": "defaultkavy",
|
||||||
"email": "defaultkavy@gmail.com",
|
"email": "defaultkavy@gmail.com",
|
||||||
@ -8,18 +9,14 @@
|
|||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/defaultkavy/fluentx.git"
|
"url": "git+https://github.com/defaultkavy/elexis.git"
|
||||||
},
|
},
|
||||||
"module": "index.ts",
|
"module": "index.ts",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/defaultkavy/fluentx/issues"
|
"url": "https://github.com/defaultkavy/elexis/issues"
|
||||||
},
|
},
|
||||||
"description": "Fast, fluent, simple web builder",
|
"homepage": "https://github.com/defaultkavy/elexis",
|
||||||
"homepage": "https://github.com/defaultkavy/fluentx",
|
|
||||||
"keywords": ["web", "front-end", "lib", "fluent", "framework"],
|
"keywords": ["web", "front-end", "lib", "fluent", "framework"],
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"type": "module",
|
"type": "module"
|
||||||
"workspaces": [
|
|
||||||
"extensions/*"
|
|
||||||
]
|
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user