new: ElexisJS icons and logo
change: README.md
update: $Input.type() will convert input into Number/Check/File Input with several methods
update: $.set() [methodKey] parameter will change to [handle] function, handle() will be called when [value] parameter is $State.
add: $State .toJSON()
This commit is contained in:
defaultkavy 2024-04-26 18:41:29 +08:00
parent 74327f8eed
commit 6454ddab48
10 changed files with 183 additions and 69 deletions

View File

@ -142,18 +142,19 @@ export namespace $ {
* @param methodKey Variant key name when apply value on $State.set()
* @returns
*/
export function set<O, K extends keyof O>(
export function set<O extends Object, K extends keyof O, V>(
object: O,
key: K,
value: O[K] extends (...args: any) => any
? (undefined | $StateArgument<Parameters<O[K]>>)
: (undefined | $StateArgument<O[K]>),
methodKey?: string) {
handle?: ($state: $State<O[K]>) => 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);

1
.gitignore vendored
View File

@ -1,3 +1,2 @@
.npmignore
bun.lockb
node_modules

2
.npmignore Normal file
View File

@ -0,0 +1,2 @@
.gitignore
assets

View File

@ -1,5 +1,10 @@
# ElexisJS
TypeScript First Web Framework, for Humans.
<picture style="display: flex; justify-content: center">
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/defaultkavy/elexis/blob/assets/logo_light.png?raw=true">
<source media="(prefers-color-scheme: light)" srcset="https://github.com/defaultkavy/elexis/blob/assets/logo_dark.png?raw=true">
<img alt="Elexis Logo">
</picture>
<p style="text-align: center">Build Web in Native JavaScript Syntax</p>
> 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 `<h1>` element which have a class name "title" and text content "Hello, world!".
Run the code, we will get this body structure in DOM:
```html
<body>
<h1 class="title">Hello, world!</h1>
</body>
```
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 `<input>` element is fill with number `42`, and also `<p>` element will display `'User input value: 42'`. Now try to change input value in browser, the value text in `<p>` 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.

View File

@ -1,11 +1,9 @@
import { $Node } from "./node/$Node";
export interface $StateOption<T> {
format: (value: T) => string;
}
export class $State<T> {
readonly value: T;
readonly attributes = new Map<$Node, Set<string | number | symbol>>();
readonly attributes = new Map<Object, Set<string | number | symbol>>();
options: Partial<$StateOption<T>> = {}
constructor(value: T, options?: $StateOption<T>) {
this.value = value;
@ -15,6 +13,7 @@ export class $State<T> {
(this as Mutable<$State<T>>).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<T> {
//@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<T extends $Node, K extends keyof T>($node: T, attrName: K) {
const attrList = this.attributes.get($node)
use<O extends Object, K extends keyof O>(object: O, attrName: K) {
const attrList = this.attributes.get(object)
if (attrList) attrList.add(attrName);
else this.attributes.set($node, new Set<string | number | symbol>().add(attrName))
else this.attributes.set(object, new Set<string | number | symbol>().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> = T | $State<T | undefined>;
export type $StateArgument<T> = T | $State<T> | undefined;

View File

@ -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);

View File

@ -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<HTMLImageElement> {
constructor(options?: $ImageOptions) {
@ -81,8 +81,8 @@ export class $Image extends $HTMLElement<HTMLImageElement> {
/**HTMLImageElement base property */
src(): string;
src(src?: string | $State<string | undefined>): this;
src(src?: string | $State<string | undefined>) { return $.fluent(this, arguments, () => this.dom.src, () => $.set(this.dom, 'src', src))}
src(src: $StateArgument<string>): this;
src(src?: $StateArgument<string>) { return $.fluent(this, arguments, () => this.dom.src, () => $.set(this.dom, 'src', src))}
/**HTMLImageElement base property */
srcset(): string;

View File

@ -2,14 +2,27 @@ import { $Element, $ElementOptions } from "./$Element";
import { $State, $StateArgument } from "../$State";
export interface $InputOptions extends $ElementOptions {}
export class $Input extends $Element<HTMLInputElement> {
export class $Input<T extends string | number = string> extends $Element<HTMLInputElement> {
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<T>): this;
value(value?: $StateArgument<T>) { 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> | string, (value$) => {
this.on('input', () => {
if (value$.attributes.has(this.dom) === false) return;
if (typeof value$.value === 'string') (value$ as $State<string>).set(`${this.value()}`)
if (typeof value$.value === 'number') (value$ as unknown as $State<number>).set(Number(this.value()))
})
}))}
type(): InputType;
type<T extends InputType>(type: T): $InputType<T>;
type<T extends InputType>(type?: T) { return $.fluent(this, arguments, () => this.dom.type, () => $.set(this.dom, 'type', type)) as unknown as $InputType<T> | InputType}
capture(): string;
capture(capture: string): this;
@ -26,26 +39,6 @@ export class $Input extends $Element<HTMLInputElement> {
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<HTMLInputElement> {
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<HTMLInputElement> {
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<HTMLInputElement> {
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<HTMLInputElement> {
}
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<HTMLInputElement> {
name(): string;
name(name?: $StateArgument<string> | undefined): this;
name(name?: $StateArgument<string> | 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<string> | undefined): this;
value(value?: $StateArgument<string> | 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 }
}
}
export class $NumberInput extends $Input<number> {
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<string> {
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<string> {
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 InputType> = T extends 'number' ? $NumberInput : T extends 'radio' | 'checkbox' ? $CheckInput : T extends 'file' ? $FileInput : $Input<string>;

View File

@ -1,3 +1,4 @@
import { $StateArgument } from "../$State";
import { $Container, $ContainerOptions } from "./$Container";
export interface $LabelOptions extends $ContainerOptions {}
export class $Label extends $Container<HTMLLabelElement> {
@ -6,8 +7,8 @@ export class $Label extends $Container<HTMLLabelElement> {
}
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<string>): this;
for(name?: $StateArgument<string>) { return $.fluent(this, arguments, () => this.dom.htmlFor, () => { $.set(this.dom, 'htmlFor', name)}) }
get form() { return this.dom.form }
get control() { return this.dom.control }

View File

@ -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",