diff --git a/$index.ts b/$index.ts index cfd621d..84f0ea6 100644 --- a/$index.ts +++ b/$index.ts @@ -142,18 +142,19 @@ export namespace $ { * @param methodKey Variant key name when apply value on $State.set() * @returns */ - export function set( + export function set( object: O, key: K, value: O[K] extends (...args: any) => any ? (undefined | $StateArgument>) : (undefined | $StateArgument), - methodKey?: string) { + handle?: ($state: $State) => any) { if (value === undefined) return; - if (value instanceof $State && object instanceof Node) { - value.use(object.$, methodKey ?? key as any); + if (value instanceof $State) { + value.use(object, key); if (object[key] instanceof Function) (object[key] as Function)(value) - else object[key] = value.value; + else object[key] = value.value; + if (handle) handle(value); return; } if (object[key] instanceof Function) (object[key] as Function)(value); diff --git a/.gitignore b/.gitignore index 0b3c9c0..eb28fa7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ -.npmignore bun.lockb node_modules \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..43dbca0 --- /dev/null +++ b/.npmignore @@ -0,0 +1,2 @@ +.gitignore +assets \ No newline at end of file diff --git a/README.md b/README.md index 39d3d76..6940e57 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ -# ElexisJS -TypeScript First Web Framework, for Humans. + + + + Elexis Logo + +

Build Web in Native JavaScript Syntax

+ > ElexisJS is still in beta test now, some breaking changes might happen very often. ## What does ElexisJS bring to developer? @@ -32,13 +37,54 @@ $('h1').class('title').css({color: 'red'}) ``` ## Build your first "Hello, world!" ElexisJS project -Let's try this in your entry file: +Let's try this code in your entry file: + ```ts $(document.body).content([ $('h1').class('title').content('Hello, world!') ]) ``` +In the first line, we create a `$Container` to using Elexis API on `document.body` element. Then we see a `content` method after the container object, this method mean the following elements will be the content of container. + +We can pass an array into `content` method. In this array, we put a new `

` element which have a class name "title" and text content "Hello, world!". + +Run the code, we will get this body structure in DOM: + +```html + +

Hello, world!

+ +``` + +So far, we just simply do a hello world project that you can type less in HTML way, and these is not the point of ElexisJS. Let's figure out what ElexisJS will boost development speed in the following examples. + +## Using `$State` to sync view and data changes + +This line will create a `$State` value, usually we will put `$` sign behind variable name to mean this is a `$State` variable. + +```ts +const number$ = $.state(42); +``` + +This `$State` value has been set a number `42`, which will become a number type `$State`. We can simply put this state value into any display content! + +```ts +const value$ = $.state(42); + +$(document.body).content([ + $('input').type('number').value(value$), + $('p').content(['User input value: ', value$]) +]) +``` + +You will see the `` element is fill with number `42`, and also `

` element will display `'User input value: 42'`. Now try to change input value in browser, the value text in `

` element will be synced by your input! + +Using `set` method to set value of `$State`, all displayed content of `value$` will be synced. +```ts +value$.set(0) +``` + ## Extensions 1. [@elexis/router](https://github.com/elexisjs/router): Router for Single Page App. 2. [@elexis/layout](https://github.com/elexisjs/layout): Build waterfall/justified layout with automatic compute content size and position. \ No newline at end of file diff --git a/lib/$State.ts b/lib/$State.ts index c622b70..49cda3e 100644 --- a/lib/$State.ts +++ b/lib/$State.ts @@ -1,11 +1,9 @@ -import { $Node } from "./node/$Node"; - export interface $StateOption { format: (value: T) => string; } export class $State { readonly value: T; - readonly attributes = new Map<$Node, Set>(); + readonly attributes = new Map>(); options: Partial<$StateOption> = {} constructor(value: T, options?: $StateOption) { this.value = value; @@ -15,6 +13,7 @@ export class $State { (this as Mutable<$State>).value = value; for (const [node, attrList] of this.attributes.entries()) { for (const attr of attrList) { + console.debug(node, attr) //@ts-expect-error if (node[attr] instanceof Function) { //@ts-expect-error @@ -22,20 +21,41 @@ export class $State { //@ts-expect-error else node[attr](value) } + else if (attr in node) { + //@ts-expect-error + node[attr] = value + } } } } toString(): string { if (this.options.format) return this.options.format(this.value); + if (this.value instanceof Object) return JSON.stringify(this.toJSON()); return `${this.value}` } - use($node: T, attrName: K) { - const attrList = this.attributes.get($node) + use(object: O, attrName: K) { + const attrList = this.attributes.get(object) if (attrList) attrList.add(attrName); - else this.attributes.set($node, new Set().add(attrName)) + else this.attributes.set(object, new Set().add(attrName)) + } + + toJSON(): Object { + if (this.value instanceof $State) return this.value.toJSON(); + if (this.value instanceof Object) return $State.toJSON(this.value); + else return this.toString(); + } + + static toJSON(object: Object): Object { + const data = {}; + for (let [key, value] of Object.entries(object)) { + if (value instanceof $State) value = value.toJSON(); + else if (value instanceof Object) $State.toJSON(value); + Object.assign(data, {[key]: value}) + } + return data; } }; -export type $StateArgument = T | $State; \ No newline at end of file +export type $StateArgument = T | $State | undefined; \ No newline at end of file diff --git a/lib/$Util.ts b/lib/$Util.ts index 33d3a4d..10947b9 100644 --- a/lib/$Util.ts +++ b/lib/$Util.ts @@ -46,6 +46,7 @@ export namespace $Util { else if (element instanceof HTMLElement) { const instance = $.TagNameElementMap[element.tagName.toLowerCase() as keyof typeof $.TagNameElementMap]; const $node = instance === $Container + //@ts-expect-error ? new instance(element.tagName, {dom: element}) //@ts-expect-error : new instance({dom: element} as any); diff --git a/lib/node/$Image.ts b/lib/node/$Image.ts index a6b3159..fdfbe26 100644 --- a/lib/node/$Image.ts +++ b/lib/node/$Image.ts @@ -1,5 +1,5 @@ import { $HTMLElement, $HTMLElementOptions } from "./$HTMLElement"; -import { $State } from "../$State"; +import { $StateArgument } from "../$State"; export interface $ImageOptions extends $HTMLElementOptions {} export class $Image extends $HTMLElement { constructor(options?: $ImageOptions) { @@ -81,8 +81,8 @@ export class $Image extends $HTMLElement { /**HTMLImageElement base property */ src(): string; - src(src?: string | $State): this; - src(src?: string | $State) { return $.fluent(this, arguments, () => this.dom.src, () => $.set(this.dom, 'src', src))} + src(src: $StateArgument): this; + src(src?: $StateArgument) { return $.fluent(this, arguments, () => this.dom.src, () => $.set(this.dom, 'src', src))} /**HTMLImageElement base property */ srcset(): string; diff --git a/lib/node/$Input.ts b/lib/node/$Input.ts index 9f22844..7cbb288 100644 --- a/lib/node/$Input.ts +++ b/lib/node/$Input.ts @@ -2,14 +2,27 @@ import { $Element, $ElementOptions } from "./$Element"; import { $State, $StateArgument } from "../$State"; export interface $InputOptions extends $ElementOptions {} -export class $Input extends $Element { +export class $Input extends $Element { constructor(options?: $InputOptions) { super('input', options); } - accept(): string[] - accept(...filetype: string[]): this - accept(...filetype: string[]) { return $.fluent(this, arguments, () => this.dom.accept.split(','), () => this.dom.accept = filetype.toString() )} + value(): T; + value(value: $StateArgument): this; + value(value?: $StateArgument) { return $.fluent(this, arguments, () => { + if (this.type() === 'number') return Number(this.dom.value); + return this.dom.value as T; + }, () => $.set(this.dom, 'value', value as $State | string, (value$) => { + this.on('input', () => { + if (value$.attributes.has(this.dom) === false) return; + if (typeof value$.value === 'string') (value$ as $State).set(`${this.value()}`) + if (typeof value$.value === 'number') (value$ as unknown as $State).set(Number(this.value())) + }) + }))} + + type(): InputType; + type(type: T): $InputType; + type(type?: T) { return $.fluent(this, arguments, () => this.dom.type, () => $.set(this.dom, 'type', type)) as unknown as $InputType | InputType} capture(): string; capture(capture: string): this; @@ -26,26 +39,6 @@ export class $Input extends $Element { width(): number; width(wdith: number): this; width(width?: number) { return $.fluent(this, arguments, () => this.dom.width, () => $.set(this.dom, 'width', width))} - - checked(): boolean; - checked(boolean: boolean): this; - checked(boolean?: boolean) { return $.fluent(this, arguments, () => this.dom.checked, () => $.set(this.dom, 'checked', boolean))} - - max(): number; - max(max: number): this; - max(max?: number) { return $.fluent(this, arguments, () => this.dom.max === '' ? null : parseInt(this.dom.min), () => $.set(this.dom, 'max', max?.toString()))} - - min(): number; - min(min: number): this; - min(min?: number) { return $.fluent(this, arguments, () => this.dom.min === '' ? null : parseInt(this.dom.min), () => $.set(this.dom, 'min', min?.toString()))} - - maxLength(): number; - maxLength(maxLength: number): this; - maxLength(maxLength?: number) { return $.fluent(this, arguments, () => this.dom.maxLength, () => $.set(this.dom, 'maxLength', maxLength))} - - minLength(): number; - minLength(minLength: number): this; - minLength(minLength?: number) { return $.fluent(this, arguments, () => this.dom.minLength, () => $.set(this.dom, 'minLength', minLength))} autocomplete(): AutoFill; autocomplete(autocomplete: AutoFill): this; @@ -55,10 +48,6 @@ export class $Input extends $Element { defaultValue(defaultValue: string): this; defaultValue(defaultValue?: string) { return $.fluent(this, arguments, () => this.dom.defaultValue, () => $.set(this.dom, 'defaultValue', defaultValue))} - defaultChecked(): boolean; - defaultChecked(defaultChecked: boolean): this; - defaultChecked(defaultChecked?: boolean) { return $.fluent(this, arguments, () => this.dom.defaultChecked, () => $.set(this.dom, 'defaultChecked', defaultChecked))} - dirName(): string; dirName(dirName: string): this; dirName(dirName?: string) { return $.fluent(this, arguments, () => this.dom.dirName, () => $.set(this.dom, 'dirName', dirName))} @@ -67,10 +56,6 @@ export class $Input extends $Element { disabled(disabled: boolean): this; disabled(disabled?: boolean) { return $.fluent(this, arguments, () => this.dom.disabled, () => $.set(this.dom, 'disabled', disabled))} - multiple(): boolean; - multiple(multiple: boolean): this; - multiple(multiple?: boolean) { return $.fluent(this, arguments, () => this.dom.multiple, () => $.set(this.dom, 'multiple', multiple))} - pattern(): string; pattern(pattern: string): this; pattern(pattern?: string) { return $.fluent(this, arguments, () => this.dom.pattern, () => $.set(this.dom, 'pattern', pattern))} @@ -106,14 +91,6 @@ export class $Input extends $Element { src(): string; src(src: string): this; src(src?: string) { return $.fluent(this, arguments, () => this.dom.src, () => $.set(this.dom, 'src', src))} - - step(): number; - step(step: number): this; - step(step?: number) { return $.fluent(this, arguments, () => Number(this.dom.step), () => $.set(this.dom, 'step', step?.toString()))} - - 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; @@ -142,8 +119,6 @@ export class $Input extends $Element { } setSelectionRange(start: number | null, end: number | null, direction?: SelectionDirection) { this.dom.setSelectionRange(start, end, direction); return this } showPicker() { this.dom.showPicker(); return this } - stepDown() { this.dom.stepDown(); return this } - stepUp() { this.dom.stepUp(); return this } checkValidity() { return this.dom.checkValidity() } reportValidity() { return this.dom.reportValidity() } @@ -174,14 +149,83 @@ export class $Input extends $Element { name(): string; name(name?: $StateArgument | undefined): this; name(name?: $StateArgument | undefined) { return $.fluent(this, arguments, () => this.dom.name, () => $.set(this.dom, 'name', name))} + + maxLength(): number; + maxLength(maxLength: number): this; + maxLength(maxLength?: number) { return $.fluent(this, arguments, () => this.dom.maxLength, () => $.set(this.dom, 'maxLength', maxLength))} - value(): string; - value(value: $StateArgument | undefined): this; - value(value?: $StateArgument | undefined) { return $.fluent(this, arguments, () => this.dom.value, () => $.set(this.dom, 'value', value))} + minLength(): number; + minLength(minLength: number): this; + minLength(minLength?: number) { return $.fluent(this, arguments, () => this.dom.minLength, () => $.set(this.dom, 'minLength', minLength))} get form() { return this.dom.form ? $(this.dom.form) : null } get labels() { return Array.from(this.dom.labels ?? []).map(label => $(label)) } get validationMessage() { return this.dom.validationMessage } get validity() { return this.dom.validity } get willValidate() { return this.dom.willValidate } -} \ No newline at end of file +} + +export class $NumberInput extends $Input { + constructor(options?: $InputOptions) { + super(options) + this.type('number') + } + + static from($input: $Input) { + return $.mixin($Input, this) as $NumberInput; + } + stepDown() { this.dom.stepDown(); return this } + stepUp() { this.dom.stepUp(); return this } + + max(): number; + max(max: number): this; + max(max?: number) { return $.fluent(this, arguments, () => this.dom.max === '' ? null : parseInt(this.dom.min), () => $.set(this.dom, 'max', max?.toString()))} + + min(): number; + min(min: number): this; + min(min?: number) { return $.fluent(this, arguments, () => this.dom.min === '' ? null : parseInt(this.dom.min), () => $.set(this.dom, 'min', min?.toString()))} + + step(): number; + step(step: number): this; + step(step?: number) { return $.fluent(this, arguments, () => Number(this.dom.step), () => $.set(this.dom, 'step', step?.toString()))} +} + +export class $CheckInput extends $Input { + constructor(options?: $InputOptions) { + super(options) + this.type('radio') + } + + static from($input: $Input) { + return $.mixin($Input, this) as $CheckInput; + } + + checked(): boolean; + checked(boolean: boolean): this; + checked(boolean?: boolean) { return $.fluent(this, arguments, () => this.dom.checked, () => $.set(this.dom, 'checked', boolean))} + + defaultChecked(): boolean; + defaultChecked(defaultChecked: boolean): this; + defaultChecked(defaultChecked?: boolean) { return $.fluent(this, arguments, () => this.dom.defaultChecked, () => $.set(this.dom, 'defaultChecked', defaultChecked))} +} + +export class $FileInput extends $Input { + constructor(options?: $InputOptions) { + super(options) + this.type('file') + } + + static from($input: $Input) { + return $.mixin($Input, this) as $CheckInput; + } + + multiple(): boolean; + multiple(multiple: boolean): this; + multiple(multiple?: boolean) { return $.fluent(this, arguments, () => this.dom.multiple, () => $.set(this.dom, 'multiple', multiple))} + + accept(): string[] + accept(...filetype: string[]): this + accept(...filetype: string[]) { return $.fluent(this, arguments, () => this.dom.accept.split(','), () => this.dom.accept = filetype.toString() )} +} + +export type $InputType = T extends 'number' ? $NumberInput : T extends 'radio' | 'checkbox' ? $CheckInput : T extends 'file' ? $FileInput : $Input; \ No newline at end of file diff --git a/lib/node/$Label.ts b/lib/node/$Label.ts index 1d4a937..976d80b 100644 --- a/lib/node/$Label.ts +++ b/lib/node/$Label.ts @@ -1,3 +1,4 @@ +import { $StateArgument } from "../$State"; import { $Container, $ContainerOptions } from "./$Container"; export interface $LabelOptions extends $ContainerOptions {} export class $Label extends $Container { @@ -6,8 +7,8 @@ export class $Label extends $Container { } for(): string; - for(name?: string): this; - for(name?: string) { return $.fluent(this, arguments, () => this.dom.htmlFor, () => { $.set(this.dom, 'htmlFor', name, 'for')}) } + for(name: $StateArgument): this; + for(name?: $StateArgument) { return $.fluent(this, arguments, () => this.dom.htmlFor, () => { $.set(this.dom, 'htmlFor', name)}) } get form() { return this.dom.form } get control() { return this.dom.control } diff --git a/package.json b/package.json index 283c2ba..4c36500 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "elexis", - "description": "Web library design for JS/TS lover.", - "version": "0.1.0", + "description": "Build Web in Native JavaScript Syntax", + "version": "0.2.0", "author": { "name": "defaultkavy", "email": "defaultkavy@gmail.com",