v0.2.0
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:
parent
74327f8eed
commit
18bbc4922a
11
$index.ts
11
$index.ts
@ -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
1
.gitignore
vendored
@ -1,3 +1,2 @@
|
||||
.npmignore
|
||||
bun.lockb
|
||||
node_modules
|
2
.npmignore
Normal file
2
.npmignore
Normal file
@ -0,0 +1,2 @@
|
||||
.gitignore
|
||||
assets
|
52
README.md
52
README.md
@ -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/assets/logo_light.png">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://github.com/defaultkavy/elexis/assets/logo_dark.png">
|
||||
<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.
|
BIN
assets/icon_dark.png
Normal file
BIN
assets/icon_dark.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 57 KiB |
BIN
assets/icon_light.png
Normal file
BIN
assets/icon_light.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 70 KiB |
BIN
assets/logo_dark.png
Normal file
BIN
assets/logo_dark.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
BIN
assets/logo_light.png
Normal file
BIN
assets/logo_light.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
@ -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;
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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>;
|
@ -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 }
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user