diff --git a/README.md b/README.md index 47a7d35..bb1feec 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ $('a') $('h1').class('amazing-title').css({color: 'red'}).content('Fluuuuuuuuuuuuent!') ``` -## Router? I got you. +## Single Page App with Router ```ts const router = new Router('/') // example.com @@ -36,10 +36,8 @@ const router = new Router('/') })) .listen() // start resolve pathname and listen state change -``` -## Single Page App -```ts +// prevent jump to other page from link $.anchorPreventDefault = true; $.anchorHandler = (url) => { router.open(url) } diff --git a/index.ts b/index.ts index 0ff39b1..ebcaea2 100644 --- a/index.ts +++ b/index.ts @@ -13,6 +13,7 @@ declare global { type Autocapitalize = 'none' | 'off' | 'sentences' | 'on' | 'words' | 'characters'; 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 InputMode = "" | "none" | "text" | "decimal" | "numeric" | "tel" | "search" | "email" | "url"; type ButtonType = "submit" | "reset" | "button" | "menu"; type TextDirection = 'ltr' | 'rtl' | 'auto' | ''; type ImageDecoding = "async" | "sync" | "auto"; diff --git a/lib/$Container.ts b/lib/$Container.ts index dd2eca7..8cce25d 100644 --- a/lib/$Container.ts +++ b/lib/$Container.ts @@ -16,7 +16,7 @@ export class $Container extends $Element * @example Element.content([$('div')]) * Element.content('Hello World')*/ content(children: $ContainerContentBuilder): this { return $.fluent(this, arguments, () => this, () => { - this.children.removeAll(); + this.children.removeAll(false); this.insert(children); })} diff --git a/lib/$Element.ts b/lib/$Element.ts index 0edad20..62d9c4f 100644 --- a/lib/$Element.ts +++ b/lib/$Element.ts @@ -105,11 +105,17 @@ export class $Element extends $Node { hidden(hidden?: boolean): this; 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; } attachInternals() { return this.dom.attachInternals(); } hidePopover() { this.dom.hidePopover(); return this; } showPopover() { this.dom.showPopover(); 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) { const animation = this.dom.animate(keyframes, options); @@ -125,4 +131,5 @@ export class $Element extends $Node { get offsetParent() { return $(this.dom.offsetParent) } get offsetTop() { return this.dom.offsetTop } get offsetWidth() { return this.dom.offsetWidth } + get dataset() { return this.dom.dataset } } \ No newline at end of file diff --git a/lib/$Input.ts b/lib/$Input.ts index 6b75279..1b5e7ce 100644 --- a/lib/$Input.ts +++ b/lib/$Input.ts @@ -117,6 +117,10 @@ export class $Input extends $Element { type(): InputType; type(type: InputType): this; 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: Date | null): this; diff --git a/lib/$NodeManager.ts b/lib/$NodeManager.ts index 8c2888a..45d0b9e 100644 --- a/lib/$NodeManager.ts +++ b/lib/$NodeManager.ts @@ -29,8 +29,9 @@ export class $NodeManager { return this; } - removeAll() { - this.elementList.forEach(ele => this.remove(ele)) + removeAll(render = true) { + this.elementList.forEach(ele => this.remove(ele)); + if (render) this.render(); } replace(target: $Node, replace: $Node) { diff --git a/lib/Router/README.md b/lib/Router/README.md new file mode 100644 index 0000000..f5731d5 --- /dev/null +++ b/lib/Router/README.md @@ -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 +}) +``` \ No newline at end of file diff --git a/lib/Router/Route.ts b/lib/Router/Route.ts index 69c4984..a92469a 100644 --- a/lib/Router/Route.ts +++ b/lib/Router/Route.ts @@ -35,8 +35,8 @@ export class RouteRecord { } export interface RouteRecordEventMap { - 'open': [path: string, record: RouteRecord]; - 'load': [path: string, record: RouteRecord]; + 'open': [{path: string, record: RouteRecord}]; + 'load': [{path: string, record: RouteRecord}]; } export interface RouteRequest { diff --git a/lib/Router/Router.ts b/lib/Router/Router.ts index 9a19acd..2595f98 100644 --- a/lib/Router/Router.ts +++ b/lib/Router/Router.ts @@ -34,7 +34,7 @@ export class Router { addEventListener('popstate', this.popstate) $.routers.add(this); this.resolvePath(); - this.events.fire('pathchange', location.href, 'Forward'); + this.events.fire('pathchange', {path: location.href, navigation: 'Forward'}); return this; } @@ -46,7 +46,7 @@ export class Router { const routeData: RouteData = { index: this.index, data: {} }; history.pushState(routeData, '', path); $.routers.forEach(router => router.resolvePath()) - this.events.fire('pathchange', path, 'Forward'); + this.events.fire('pathchange', {path, navigation: 'Forward'}); return this; } @@ -56,7 +56,7 @@ export class Router { replace(path: string) { history.replaceState({index: this.index}, '', path) $.routers.forEach(router => router.resolvePath(path)); - this.events.fire('pathchange', path, 'Forward'); + this.events.fire('pathchange', {path, navigation: 'Forward'}); return this; } @@ -73,19 +73,19 @@ export class Router { else if (history.state.index < this.index) { } this.index = history.state.index; this.resolvePath(); - this.events.fire('pathchange', location.pathname, 'Forward'); + this.events.fire('pathchange', {path: location.pathname, navigation: 'Forward'}); }).bind(this) private resolvePath(path = location.pathname) { if (!path.startsWith(this.basePath)) return; - path = path.replace(this.basePath, '/').replace('//', '/') + 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.content(record.content); - record.events.fire('open', path, record); + record.events.fire('open', {path, record}); return true; } return false; @@ -96,15 +96,15 @@ export class Router { params: data, record: record, loaded: () => { - record.events.fire('load', pathId, record); - this.events.fire('load', pathId); + record.events.fire('load', {path: pathId, record}); + this.events.fire('load', {path: pathId}); } }); if (typeof content === 'string') content = new $Text(content); (record as Mutable).content = content; this.recordMap.set(pathId, record); this.view.content(content); - record.events.fire('open', path, record); + record.events.fire('open', {path, record}); found = true; } 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 { - pathchange: [path: string, navigation: 'Back' | 'Forward']; - notfound: [path: string]; - load: [path: string]; + pathchange: [{path: string, navigation: 'Back' | 'Forward'}]; + notfound: [{path: string, preventDefault: () => void}]; + load: [{path: string}]; } type RouteData = { diff --git a/package.json b/package.json index e344b52..507307f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "fluentx", "description": "Fast, fluent, simple web builder", - "version": "0.0.2", + "version": "0.0.3", "type": "module", "module": "index.ts", "author": {