Super concise chained call UI library
Demo | Versions | MessageBoard
Link-dom is an extremely concise imperative UI library with chained call features and no third-party dependencies. Its minimum size is only 7.7kb.
Link-dom provides the createStore method for simple state management.
In tool library projects that require UI development, if native js is used to write UI, it will be difficult to manage and maintain, and the code volume will be large. And introducing UI libraries such as Vue and React will greatly increase the package size and even be a bit of overkill. The birth of link-dom is precisely to meet such scenarios of small-scale UI usage. UI is described by JS imperative and componentized UI management is achieved.
- Imperative UI, supports chained calls and has no third-party dependencies.
- Has friendly type declaration support, covering attributes, styles, Dom tags, etc.
- Has good css-in-js support and supports nested rules similar to less.
- Support reactive css-in-js.
- Supports state management.
npm i link-dom
<script src="https://cdn.jsdelivr.net/npm/link-dom"></script>
<script>
console.log(window.LinkDom)
</script>
Dom is a layer of encapsulation of dom elements, and all operations on the dom are handled inside IUIEle. Chaining and friendly type hints are also implemented in Dom
Generate an Dom element with dom.xxx
import {mount, dom} from 'link-dom';
const hello = dom.div.text('Hello');
mount(hello, 'body');
Dom declaration
export declare class Dom<T extends HTMLElement = HTMLElement> {
__ld_type: LinkDomType;
el: T;
constructor(key: (keyof HTMLElementTagNameMap) | T);
private _ur;
class(): string;
class(val: IReactiveLike<string>): this;
id(): string;
id(val: IReactiveLike<string>): this;
addClass(name: string): this;
removeClass(name: string): this;
hasClass(name: string): boolean;
toggleClass(name: string, force?: boolean): boolean;
replaceClass(n: string, old: string): this;
remove(): this;
text(): string;
text(val: IReactiveLike<string | number>): this;
private __mounted?;
mounted(v: (el: Dom) => void): this;
attr(name: {
[prop in IAttrKey]?: any;
} | Record<string, any>): this;
attr(name: IAttrKey): string;
attr(name: string): string;
attr(name: IAttrKey, value: any): this;
attr(name: string, value: any): this;
removeAttr(key: string): this;
private __xr_funcs;
func(k: string): (...args: any[]) => any;
func(k: string, v: (...args: any[]) => any): this;
data(name: Record<string, any>): this;
data(name: string): any | null;
data(name: string, value: any): this;
style(name: IStyle | Record<string, any>): this;
style(name: IStyleKey | string): string;
style<T extends IStyleKey>(name: T, value: IStyle[T]): this;
value(): string;
value(val: IReactiveLike<string | number>): this;
html(): string;
html(val: IReactiveLike<string | number>): this;
outerHtml(): string;
outerHtml(val: IReactiveLike<string | number>): this;
child<T extends HTMLElement = HTMLElement>(i: number): Dom<T> | null;
next<T extends HTMLElement = HTMLElement>(): Dom<T> | null;
prev<T extends HTMLElement = HTMLElement>(): Dom<T> | null;
firstChild<T extends HTMLElement = HTMLElement>(): Dom<T> | null;
lastChild<T extends HTMLElement = HTMLElement>(): Dom<T> | null;
brothers(): Dom<HTMLElement>[];
children(): Dom<HTMLElement>[];
click(value: IEventObject<this>): this;
on(name: Partial<Record<IEventKey, IEventObject<this>>>): this;
on(name: IEventKey, value?: IEventObject<this>): this;
append(...doms: IChild[]): this;
ref(v: Dom): this;
hide(): this;
display(display?: IReactiveLike<IStyle["display"]>): this;
show(visible: IReactiveLike<boolean>, display?: IStyle["display"]): this;
query<T extends HTMLElement = HTMLElement>(selector: string, one: true): Dom<T>;
query<T extends HTMLElement = HTMLElement>(selector: string, one?: false): Dom<T>[];
src(): string;
src(v: IReactiveLike<string>): this;
parent<T extends HTMLElement = HTMLElement>(i?: number): Dom<T> | null;
empty(): this;
name(): string;
name(v: IReactiveLike<string>): this;
findName(name: string): Dom<HTMLElement>;
find(v: string): Dom<HTMLElement>;
type(name: "text" | "number" | "password" | "checkbox" | "radio" | "color" | "range" | "submit" | "reset" | "input" | "date" | "email" | "tel"): this;
bind(v: IReactiveLike<string | number | boolean>): this;
}
Transform Selector of HTMLElement into Dom
const body = dom('body');
// or
const body = dom(document.body);
import {style} from 'link-dom';
style({
'.parent': {
fontSize: '12px',
'*': {color: '#000'},
'.child': {
color: '#444',
'&.active': {color: '#f44'}
},
}
})
reactive style
import {style} from 'link-dom';
const color = ref('#000')
style({
'.parent': {
fontSize: '12px',
'*': {color},
'.child': {
color: ()=>color.value,
'&.active': {color: '#f44'}
},
}
})
import {mount, dom} from 'link-dom';
function Main(){
return dom.div.text('Hello World!')
.on('click', (e, self)=>{
console.log('Hello', self)
});
}
mount(Main(), 'body');
Use a decorator
import {mount, dom} from 'link-dom';
function Main(){
return dom.div.text('Hello World!')
.on('click', {
stop: true,
listener: ()=>{
console.log('Hello')
}
});
}
mount(Main(), 'body');
Support 'prevent' | 'stop' | 'capture' | 'once' | 'self'
decorators.
import {mount, dom} from 'link-dom';
mount(dom.div.mounted(el=>{
console.log('mounted', el);
}), 'body');
import {collectRef, mount, dom} from 'link-dom';
function Main(){
const refs = collectRef('hello');
return dom.div.ref(refs.hello)
.click(()=>{console.log(refs.hello.text())})
.text('Hello World!');
}
mount(Main(), 'body');
A simple state management and related APIs to use
import {createStore, mount, computed} from 'link-dom';
function Counter () {
const store = createStore({
count: 0,
});
const increase = () => {
store.count += 1;
};
const unsub = store.$sub('count', (v, pv) => {
console.log(`Subscribe Count Change value=,`, v, `; prevValue=`, pv);
});
const countAdd1 = computed(()=>store.count + 1);
return dom.div.append(
dom.input.type('number').bind(store.count),
dom.span.text(() => `count=${store.count}; count+1=${countAdd1.value}`),
dom.span.text('ShowText').show(()=>store.count % 2 === 1),
dom.button.text('addCount').click(increase),
dom.button.text('UnSubscribe').click(unsub)
);
}
mount(Counter(), 'body');
Create a state store
import {createStore} from 'link-dom';
const store = createStore({
count: 0,
});
Create a state store
import {ref} from 'link-dom';
const count = ref(0);
console.log(count.value);
import {computed} from 'link-dom';
const store = createStore({
count: 0,
});
const countAdd1 = computed(()=>{
return store.count + 1;
})
const countAdd2 = computed(()=>{
return countAdd1.value + 1;
})
const countSetDemo = computed(()=>{
return store.count + 1;
}, (v)=>{
store.count = v - 1;
});
countAdd1.sub((v, old)=>{
console.log('sub:', v, old);
});
Used for bidirectional binding of input type elements, the data inside the bind method does not need to be wrapped using react
const input = dom.input.bind(store.count)
bind set Computed
const countSetDemo = computed(()=>{
return store.count + 1;
}, (v)=>{
store.count = v - 1;
});
const input = dom.input.bind(countSetDemo)
dom.text('hello'); // create textNode
dom.text.text('hello');
dom.comment('hello'); // create Comment
dom.comment.text('hello');
dom.frag.append(/**/); // create DocumentFragment
dom.svg.html(/**/); // create SVGElement
dom.fromHTML('<a>1</a>'); // from html string
dom.query('.test'); // query elements
dom.find('.test'); // find first element
Subscribe to reactive data changes, which returns an unsubscribe
const unsub = store.$sub('count', (value, prevValue)=>{
console.log(value, prevValue);
});
// Call unsub can unsubscribe
Cancel Subscribe
let handler = (value, prevValue)=>{
console.log(value, prevValue);
}
store.$sub('count', handler);
store.$unsub('count', handler);
store.$unsub('count'); // 不传入第二个参数可以取消指定状态的所有订阅
import {watch, createStore, ref, computed} from 'link-dom';
const store = createStore({
count: 0,
});
const count = ref(0);
const countAdd1 = computed(()=>{
return store.count + count + 1;
});
watch(store.count, (v, old)=>{console.log('watch store', v, old)});
watch(count, (v, old)=>{console.log('watch ref', v, old)});
watch(countAdd1, (v, old)=>{console.log('watch computed', v, old)});
watch(()=>countAdd1.value, (v, old)=>{console.log('watch countAdd1', v, old)});
watch(()=>store.count + 1, (v, old)=>{console.log('watch count+1', v, old)});
store.count ++;
count.value ++;
unwatch
import {watch, ref} from 'link-dom';
const count = ref(0);
const unwatch = watch(count, (v, pref)=>{
console.log('watch', ref)
});
unwatch();
import {dom, collectRef, UIEle, mount} from 'link-dom';
function List () {
const refs = collectRef('list');
const list = ['1', '2', '3'];
const SingleChild = (children: UIEle|string|number) => {
return dom.span.append(children);
};
let index = 0;
return dom.div.ref(refs.list).append(
dom.button.text('add').click(() => {
index ++;
list.push(`${index}`);
refs.list.append(SingleChild(index));
}),
list.map(item => SingleChild(item))
);
}
mount(List(), 'body');
Minimal version
import {dom, createStore, mount} from 'link-dom';
function Counter () {
const store = createStore({ count: 0 });
return dom.button.text(()=>`count is ${store.count}`)
.click(() => store.count++);
}
mount(Counter(), 'body');
With Subscribe
import {dom, createStore, react, mount} from 'link-dom';
function Counter () {
const store = createStore({
count: 0,
});
const increase = () => {
store.count += 1;
};
const unsub = store.$sub('count', (v, pv) => {
console.log(`Subscribe Count Change value=,`, v, `; prevValue=`, pv);
});
const countAdd1 = computed(()=>store.count + 1);
return dom.div.append(
dom.input.type('number').bind(store.count),
dom.span.text(() => `count=${store.count}; count+1=${countAdd1.value}`),
dom.span.text('ShowText').show(()=>store.count % 2 === 1),
dom.button.text('addCount').click(increase),
dom.button.text('UnSubscribe').click(unsub)
);
}
mount(Counter(), 'body');