version 0.0.3

con: Route/Router event callback function use object to pass params
change: $NodeManager.removeAll() with render params
add: type InputMode
update: README
This commit is contained in:
defaultkavy 2024-03-28 20:03:03 +08:00
parent 1489b1dba5
commit 0b3ca308d6
10 changed files with 118 additions and 23 deletions

View File

@ -22,7 +22,7 @@ $('a')
$('h1').class('amazing-title').css({color: 'red'}).content('Fluuuuuuuuuuuuent!') $('h1').class('amazing-title').css({color: 'red'}).content('Fluuuuuuuuuuuuent!')
``` ```
## Router? I got you. ## Single Page App with Router
```ts ```ts
const router = new Router('/') const router = new Router('/')
// example.com // example.com
@ -36,10 +36,8 @@ const router = new Router('/')
})) }))
.listen() // start resolve pathname and listen state change .listen() // start resolve pathname and listen state change
```
## Single Page App // prevent jump to other page from <a> link
```ts
$.anchorPreventDefault = true; $.anchorPreventDefault = true;
$.anchorHandler = (url) => { router.open(url) } $.anchorHandler = (url) => { router.open(url) }

View File

@ -13,6 +13,7 @@ declare global {
type Autocapitalize = 'none' | 'off' | 'sentences' | 'on' | 'words' | 'characters'; type Autocapitalize = 'none' | 'off' | 'sentences' | 'on' | 'words' | 'characters';
type SelectionDirection = "forward" | "backward" | "none"; type SelectionDirection = "forward" | "backward" | "none";
type InputType = "button" | "checkbox" | "color" | "date" | "datetime-local" | "email" | "file" | "hidden" | "image" | "month" | "number" | "password" | "radio" | "range" | "reset" | "search" | "submit" | "tel" | "text" | "time" | "url" | "week"; type InputType = "button" | "checkbox" | "color" | "date" | "datetime-local" | "email" | "file" | "hidden" | "image" | "month" | "number" | "password" | "radio" | "range" | "reset" | "search" | "submit" | "tel" | "text" | "time" | "url" | "week";
type InputMode = "" | "none" | "text" | "decimal" | "numeric" | "tel" | "search" | "email" | "url";
type ButtonType = "submit" | "reset" | "button" | "menu"; type ButtonType = "submit" | "reset" | "button" | "menu";
type TextDirection = 'ltr' | 'rtl' | 'auto' | ''; type TextDirection = 'ltr' | 'rtl' | 'auto' | '';
type ImageDecoding = "async" | "sync" | "auto"; type ImageDecoding = "async" | "sync" | "auto";

View File

@ -16,7 +16,7 @@ export class $Container<H extends HTMLElement = HTMLElement> extends $Element<H>
* @example Element.content([$('div')]) * @example Element.content([$('div')])
* Element.content('Hello World')*/ * Element.content('Hello World')*/
content(children: $ContainerContentBuilder<this>): this { return $.fluent(this, arguments, () => this, () => { content(children: $ContainerContentBuilder<this>): this { return $.fluent(this, arguments, () => this, () => {
this.children.removeAll(); this.children.removeAll(false);
this.insert(children); this.insert(children);
})} })}

View File

@ -105,11 +105,17 @@ export class $Element<H extends HTMLElement = HTMLElement> extends $Node<H> {
hidden(hidden?: boolean): this; hidden(hidden?: boolean): this;
hidden(hidden?: boolean) { return $.fluent(this, arguments, () => this.dom.hidden, () => $.set(this.dom, 'hidden', hidden))} hidden(hidden?: boolean) { return $.fluent(this, arguments, () => this.dom.hidden, () => $.set(this.dom, 'hidden', hidden))}
tabIndex(): number;
tabIndex(tabIndex: number): this;
tabIndex(tabIndex?: number) { return $.fluent(this, arguments, () => this.dom.tabIndex, () => $.set(this.dom, 'tabIndex', tabIndex))}
click() { this.dom.click(); return this; } click() { this.dom.click(); return this; }
attachInternals() { return this.dom.attachInternals(); } attachInternals() { return this.dom.attachInternals(); }
hidePopover() { this.dom.hidePopover(); return this; } hidePopover() { this.dom.hidePopover(); return this; }
showPopover() { this.dom.showPopover(); return this; } showPopover() { this.dom.showPopover(); return this; }
togglePopover() { this.dom.togglePopover(); return this; } togglePopover() { this.dom.togglePopover(); return this; }
focus() { this.dom.focus(); return this; }
blur() { this.dom.blur(); return this; }
animate(keyframes: Keyframe[] | PropertyIndexedKeyframes | null, options?: number | KeyframeAnimationOptions, callback?: (animation: Animation) => void) { animate(keyframes: Keyframe[] | PropertyIndexedKeyframes | null, options?: number | KeyframeAnimationOptions, callback?: (animation: Animation) => void) {
const animation = this.dom.animate(keyframes, options); const animation = this.dom.animate(keyframes, options);
@ -125,4 +131,5 @@ export class $Element<H extends HTMLElement = HTMLElement> extends $Node<H> {
get offsetParent() { return $(this.dom.offsetParent) } get offsetParent() { return $(this.dom.offsetParent) }
get offsetTop() { return this.dom.offsetTop } get offsetTop() { return this.dom.offsetTop }
get offsetWidth() { return this.dom.offsetWidth } get offsetWidth() { return this.dom.offsetWidth }
get dataset() { return this.dom.dataset }
} }

View File

@ -118,6 +118,10 @@ export class $Input extends $Element<HTMLInputElement> {
type(type: InputType): this; type(type: InputType): this;
type(type?: InputType) { return $.fluent(this, arguments, () => this.dom.type, () => $.set(this.dom, 'type', type))} type(type?: InputType) { return $.fluent(this, arguments, () => this.dom.type, () => $.set(this.dom, 'type', type))}
inputMode(): InputMode;
inputMode(mode: InputMode): this;
inputMode(mode?: InputMode) { return $.fluent(this, arguments, () => this.dom.inputMode as InputMode, () => $.set(this.dom, 'inputMode', mode))}
valueAsDate(): Date | null; valueAsDate(): Date | null;
valueAsDate(date: Date | null): this; valueAsDate(date: Date | null): this;
valueAsDate(date?: Date | null) { return $.fluent(this, arguments, () => this.dom.valueAsDate, () => $.set(this.dom, 'valueAsDate', date))} valueAsDate(date?: Date | null) { return $.fluent(this, arguments, () => this.dom.valueAsDate, () => $.set(this.dom, 'valueAsDate', date))}

View File

@ -29,8 +29,9 @@ export class $NodeManager {
return this; return this;
} }
removeAll() { removeAll(render = true) {
this.elementList.forEach(ele => this.remove(ele)) this.elementList.forEach(ele => this.remove(ele));
if (render) this.render();
} }
replace(target: $Node, replace: $Node) { replace(target: $Node, replace: $Node) {

79
lib/Router/README.md Normal file
View File

@ -0,0 +1,79 @@
# 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
})
```

View File

@ -35,8 +35,8 @@ export class RouteRecord {
} }
export interface RouteRecordEventMap { export interface RouteRecordEventMap {
'open': [path: string, record: RouteRecord]; 'open': [{path: string, record: RouteRecord}];
'load': [path: string, record: RouteRecord]; 'load': [{path: string, record: RouteRecord}];
} }
export interface RouteRequest<Path extends PathResolverFn | string> { export interface RouteRequest<Path extends PathResolverFn | string> {

View File

@ -34,7 +34,7 @@ export class Router {
addEventListener('popstate', this.popstate) addEventListener('popstate', this.popstate)
$.routers.add(this); $.routers.add(this);
this.resolvePath(); this.resolvePath();
this.events.fire('pathchange', location.href, 'Forward'); this.events.fire('pathchange', {path: location.href, navigation: 'Forward'});
return this; return this;
} }
@ -46,7 +46,7 @@ export class Router {
const routeData: RouteData = { index: this.index, data: {} }; const routeData: RouteData = { index: this.index, data: {} };
history.pushState(routeData, '', path); history.pushState(routeData, '', path);
$.routers.forEach(router => router.resolvePath()) $.routers.forEach(router => router.resolvePath())
this.events.fire('pathchange', path, 'Forward'); this.events.fire('pathchange', {path, navigation: 'Forward'});
return this; return this;
} }
@ -56,7 +56,7 @@ export class Router {
replace(path: string) { replace(path: string) {
history.replaceState({index: this.index}, '', path) history.replaceState({index: this.index}, '', path)
$.routers.forEach(router => router.resolvePath(path)); $.routers.forEach(router => router.resolvePath(path));
this.events.fire('pathchange', path, 'Forward'); this.events.fire('pathchange', {path, navigation: 'Forward'});
return this; return this;
} }
@ -73,19 +73,19 @@ export class Router {
else if (history.state.index < this.index) { } else if (history.state.index < this.index) { }
this.index = history.state.index; this.index = history.state.index;
this.resolvePath(); this.resolvePath();
this.events.fire('pathchange', location.pathname, 'Forward'); this.events.fire('pathchange', {path: location.pathname, navigation: 'Forward'});
}).bind(this) }).bind(this)
private resolvePath(path = location.pathname) { private resolvePath(path = location.pathname) {
if (!path.startsWith(this.basePath)) return; if (!path.startsWith(this.basePath)) return;
path = path.replace(this.basePath, '/').replace('//', '/') path = path.replace(this.basePath, '/').replace('//', '/');
let found = false; let found = false;
const openCached = (pathId: string) => { const openCached = (pathId: string) => {
const record = this.recordMap.get(pathId); const record = this.recordMap.get(pathId);
if (record) { if (record) {
found = true; found = true;
if (record.content && !this.view.contains(record.content)) this.view.content(record.content); if (record.content && !this.view.contains(record.content)) this.view.content(record.content);
record.events.fire('open', path, record); record.events.fire('open', {path, record});
return true; return true;
} }
return false; return false;
@ -96,15 +96,15 @@ export class Router {
params: data, params: data,
record: record, record: record,
loaded: () => { loaded: () => {
record.events.fire('load', pathId, record); record.events.fire('load', {path: pathId, record});
this.events.fire('load', pathId); this.events.fire('load', {path: pathId});
} }
}); });
if (typeof content === 'string') content = new $Text(content); if (typeof content === 'string') content = new $Text(content);
(record as Mutable<RouteRecord>).content = content; (record as Mutable<RouteRecord>).content = content;
this.recordMap.set(pathId, record); this.recordMap.set(pathId, record);
this.view.content(content); this.view.content(content);
record.events.fire('open', path, record); record.events.fire('open', {path, record});
found = true; found = true;
} }
for (const [pathResolver, route] of this.routeMap.entries()) { for (const [pathResolver, route] of this.routeMap.entries()) {
@ -142,13 +142,18 @@ export class Router {
} }
} }
if (!found) this.events.fire('notfound', path); if (!found) {
let preventDefaultState = false;
const preventDefault = () => preventDefaultState = true;
this.events.fire('notfound', {path, preventDefault});
if (!preventDefaultState) this.view.children.removeAll();
}
} }
} }
interface RouterEventMap { interface RouterEventMap {
pathchange: [path: string, navigation: 'Back' | 'Forward']; pathchange: [{path: string, navigation: 'Back' | 'Forward'}];
notfound: [path: string]; notfound: [{path: string, preventDefault: () => void}];
load: [path: string]; load: [{path: string}];
} }
type RouteData = { type RouteData = {

View File

@ -1,7 +1,7 @@
{ {
"name": "fluentx", "name": "fluentx",
"description": "Fast, fluent, simple web builder", "description": "Fast, fluent, simple web builder",
"version": "0.0.2", "version": "0.0.3",
"type": "module", "type": "module",
"module": "index.ts", "module": "index.ts",
"author": { "author": {