mirror of
https://github.com/jaandrle/deka-dom-el
synced 2024-11-24 01:29:36 +01:00
📦 🐛 ✏️ updates & types & docs
This commit is contained in:
parent
3fc585d012
commit
f31808c2d6
27
dist/esm-with-observables.d.ts
vendored
27
dist/esm-with-observables.d.ts
vendored
@ -112,31 +112,24 @@ export function classListDeclarative<El extends SupportedElement>(element: El, c
|
|||||||
export function assign<El extends SupportedElement>(element: El, ...attrs_array: ElementAttributes<El>[]): El
|
export function assign<El extends SupportedElement>(element: El, ...attrs_array: ElementAttributes<El>[]): El
|
||||||
export function assignAttribute<El extends SupportedElement, ATT extends keyof ElementAttributes<El>>(element: El, attr: ATT, value: ElementAttributes<El>[ATT]): ElementAttributes<El>[ATT]
|
export function assignAttribute<El extends SupportedElement, ATT extends keyof ElementAttributes<El>>(element: El, attr: ATT, value: ElementAttributes<El>[ATT]): ElementAttributes<El>[ATT]
|
||||||
|
|
||||||
type ExtendedHTMLElementTagNameMap= ddeHTMLElementTagNameMap & CustomElementTagNameMap & ddePublicElementTagNameMap
|
type ExtendedHTMLElementTagNameMap= HTMLElementTagNameMap & CustomElementTagNameMap & ddePublicElementTagNameMap
|
||||||
export function el<
|
export function el<
|
||||||
TAG extends string,
|
TAG extends keyof ExtendedHTMLElementTagNameMap & string,
|
||||||
EL extends (TAG extends keyof ExtendedHTMLElementTagNameMap ? ExtendedHTMLElementTagNameMap[TAG] : ddeHTMLElement)
|
EL extends (TAG extends keyof ExtendedHTMLElementTagNameMap ? ExtendedHTMLElementTagNameMap[TAG] : HTMLElement)
|
||||||
>(
|
>(
|
||||||
tag_name: TAG,
|
tag_name: TAG,
|
||||||
attrs?: ElementAttributes<EL>,
|
attrs?: string | Observable<string, any> | ElementAttributes<EL>,
|
||||||
...addons: ddeElementAddon<TAG extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[TAG] : EL>[]
|
...addons: ddeElementAddon<EL>[]
|
||||||
): EL
|
): TAG extends keyof ddeHTMLElementTagNameMap ? ddeHTMLElementTagNameMap[TAG] : ddeHTMLElement
|
||||||
export function el<
|
export function el(
|
||||||
TAG extends string,
|
|
||||||
EL extends (TAG extends keyof ExtendedHTMLElementTagNameMap ? ExtendedHTMLElementTagNameMap[TAG] : ddeHTMLElement)
|
|
||||||
>(
|
|
||||||
tag_name: TAG,
|
|
||||||
attrs?: string | Observable<string, any>,
|
|
||||||
...addons: ddeElementAddon<TAG extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[TAG] : EL>[]
|
|
||||||
): EL
|
|
||||||
export function el<T>(
|
|
||||||
tag_name?: "<>",
|
tag_name?: "<>",
|
||||||
): ddeDocumentFragment
|
): ddeDocumentFragment
|
||||||
|
|
||||||
export function el<
|
export function el<
|
||||||
A extends ddeComponentAttributes,
|
A extends ddeComponentAttributes,
|
||||||
C extends (attr: Partial<A>)=> SupportedElement | DocumentFragment>(
|
C extends (attr: Partial<A>)=> SupportedElement | DocumentFragment
|
||||||
fComponent: C,
|
>(
|
||||||
|
component: C,
|
||||||
attrs?: A | string,
|
attrs?: A | string,
|
||||||
...addons: ddeElementAddon<ReturnType<C>>[]
|
...addons: ddeElementAddon<ReturnType<C>>[]
|
||||||
): ReturnType<C>
|
): ReturnType<C>
|
||||||
|
27
dist/esm.d.ts
vendored
27
dist/esm.d.ts
vendored
@ -112,31 +112,24 @@ export function classListDeclarative<El extends SupportedElement>(element: El, c
|
|||||||
export function assign<El extends SupportedElement>(element: El, ...attrs_array: ElementAttributes<El>[]): El
|
export function assign<El extends SupportedElement>(element: El, ...attrs_array: ElementAttributes<El>[]): El
|
||||||
export function assignAttribute<El extends SupportedElement, ATT extends keyof ElementAttributes<El>>(element: El, attr: ATT, value: ElementAttributes<El>[ATT]): ElementAttributes<El>[ATT]
|
export function assignAttribute<El extends SupportedElement, ATT extends keyof ElementAttributes<El>>(element: El, attr: ATT, value: ElementAttributes<El>[ATT]): ElementAttributes<El>[ATT]
|
||||||
|
|
||||||
type ExtendedHTMLElementTagNameMap= ddeHTMLElementTagNameMap & CustomElementTagNameMap & ddePublicElementTagNameMap
|
type ExtendedHTMLElementTagNameMap= HTMLElementTagNameMap & CustomElementTagNameMap & ddePublicElementTagNameMap
|
||||||
export function el<
|
export function el<
|
||||||
TAG extends string,
|
TAG extends keyof ExtendedHTMLElementTagNameMap & string,
|
||||||
EL extends (TAG extends keyof ExtendedHTMLElementTagNameMap ? ExtendedHTMLElementTagNameMap[TAG] : ddeHTMLElement)
|
EL extends (TAG extends keyof ExtendedHTMLElementTagNameMap ? ExtendedHTMLElementTagNameMap[TAG] : HTMLElement)
|
||||||
>(
|
>(
|
||||||
tag_name: TAG,
|
tag_name: TAG,
|
||||||
attrs?: ElementAttributes<EL>,
|
attrs?: string | Observable<string, any> | ElementAttributes<EL>,
|
||||||
...addons: ddeElementAddon<TAG extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[TAG] : EL>[]
|
...addons: ddeElementAddon<EL>[]
|
||||||
): EL
|
): TAG extends keyof ddeHTMLElementTagNameMap ? ddeHTMLElementTagNameMap[TAG] : ddeHTMLElement
|
||||||
export function el<
|
export function el(
|
||||||
TAG extends string,
|
|
||||||
EL extends (TAG extends keyof ExtendedHTMLElementTagNameMap ? ExtendedHTMLElementTagNameMap[TAG] : ddeHTMLElement)
|
|
||||||
>(
|
|
||||||
tag_name: TAG,
|
|
||||||
attrs?: string | Observable<string, any>,
|
|
||||||
...addons: ddeElementAddon<TAG extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[TAG] : EL>[]
|
|
||||||
): EL
|
|
||||||
export function el<T>(
|
|
||||||
tag_name?: "<>",
|
tag_name?: "<>",
|
||||||
): ddeDocumentFragment
|
): ddeDocumentFragment
|
||||||
|
|
||||||
export function el<
|
export function el<
|
||||||
A extends ddeComponentAttributes,
|
A extends ddeComponentAttributes,
|
||||||
C extends (attr: Partial<A>)=> SupportedElement | DocumentFragment>(
|
C extends (attr: Partial<A>)=> SupportedElement | DocumentFragment
|
||||||
fComponent: C,
|
>(
|
||||||
|
component: C,
|
||||||
attrs?: A | string,
|
attrs?: A | string,
|
||||||
...addons: ddeElementAddon<ReturnType<C>>[]
|
...addons: ddeElementAddon<ReturnType<C>>[]
|
||||||
): ReturnType<C>
|
): ReturnType<C>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const highlighter= await shiki.getHighlighter({
|
const highlighter= await globalThis.shiki.getHighlighter({
|
||||||
theme: "css-variables",
|
theme: "css-variables",
|
||||||
langs: ["js", "ts", "css", "html", "shell"],
|
langs: ["js", "ts", "css", "html", "shell"],
|
||||||
});
|
});
|
||||||
|
@ -145,6 +145,7 @@ main > *{
|
|||||||
--shiki-token-punctuation: var(--code);
|
--shiki-token-punctuation: var(--code);
|
||||||
--shiki-token-link: #EE0000;
|
--shiki-token-link: #EE0000;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
|
tab-size: 2;
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
}
|
}
|
||||||
.code[data-js=todo]{
|
.code[data-js=todo]{
|
||||||
|
@ -153,12 +153,13 @@ function subcomponent({ id }){
|
|||||||
</code></div><p>Strictly speaking, the imperative way of using the library is not prohibited. Just be careful (rather avoid) mixing declarative approach (using observables) and imperative manipulation of elements.</p><div class="code" data-js="todo"><!--<dde:mark type="component" name="code" host="parentElement" ssr/>--><code class="language-js">/* PSEUDO-CODE!!! */
|
</code></div><p>Strictly speaking, the imperative way of using the library is not prohibited. Just be careful (rather avoid) mixing declarative approach (using observables) and imperative manipulation of elements.</p><div class="code" data-js="todo"><!--<dde:mark type="component" name="code" host="parentElement" ssr/>--><code class="language-js">/* PSEUDO-CODE!!! */
|
||||||
import { el, on, scope } from "deka-dom-el";
|
import { el, on, scope } from "deka-dom-el";
|
||||||
function component(){
|
function component(){
|
||||||
|
const { host }= scope;
|
||||||
const ul= el("ul");
|
const ul= el("ul");
|
||||||
const ac= new AbortController();
|
const ac= new AbortController();
|
||||||
fetchAPI({ signal: ac.signal }).then(data=> {
|
fetchAPI({ signal: ac.signal }).then(data=> {
|
||||||
data.forEach(d=> ul.append(el("li", d)));
|
data.forEach(d=> ul.append(el("li", d)));
|
||||||
});
|
});
|
||||||
scope.host(
|
host(
|
||||||
/* element was remove before data fetched */
|
/* element was remove before data fetched */
|
||||||
on.disconnected(()=> ac.abort())
|
on.disconnected(()=> ac.abort())
|
||||||
);
|
);
|
||||||
|
@ -14,6 +14,7 @@ ${host}{
|
|||||||
--shiki-token-punctuation: var(--code);
|
--shiki-token-punctuation: var(--code);
|
||||||
--shiki-token-link: #EE0000;
|
--shiki-token-link: #EE0000;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
|
tab-size: 2;${""/* TODO: allow custom tab size?! */}
|
||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
}
|
}
|
||||||
${host}[data-js=todo]{
|
${host}[data-js=todo]{
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const highlighter= await shiki.getHighlighter({
|
const highlighter= await globalThis.shiki.getHighlighter({
|
||||||
theme: "css-variables",
|
theme: "css-variables",
|
||||||
langs: ["js", "ts", "css", "html", "shell"],
|
langs: ["js", "ts", "css", "html", "shell"],
|
||||||
});
|
});
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
/* PSEUDO-CODE!!! */
|
/* PSEUDO-CODE!!! */
|
||||||
import { el, on, scope } from "deka-dom-el";
|
import { el, on, scope } from "deka-dom-el";
|
||||||
function component(){
|
function component(){
|
||||||
|
const { host }= scope;
|
||||||
const ul= el("ul");
|
const ul= el("ul");
|
||||||
const ac= new AbortController();
|
const ac= new AbortController();
|
||||||
fetchAPI({ signal: ac.signal }).then(data=> {
|
fetchAPI({ signal: ac.signal }).then(data=> {
|
||||||
data.forEach(d=> ul.append(el("li", d)));
|
data.forEach(d=> ul.append(el("li", d)));
|
||||||
});
|
});
|
||||||
scope.host(
|
host(
|
||||||
/* element was remove before data fetched */
|
/* element was remove before data fetched */
|
||||||
on.disconnected(()=> ac.abort())
|
on.disconnected(()=> ac.abort())
|
||||||
);
|
);
|
||||||
|
44
examples/components/3rd-party.js
Normal file
44
examples/components/3rd-party.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { style, el, O } from '../exports.js';
|
||||||
|
const className= style.host(thirdParty).css`
|
||||||
|
:host {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const store_adapter= {
|
||||||
|
read(){ return (new URL(location)).searchParams; },
|
||||||
|
write(data){ console.log(data); history.replaceState("", "", "?"+(new URLSearchParams(data)).toString()); }
|
||||||
|
};
|
||||||
|
export function thirdParty(){
|
||||||
|
const store= O({
|
||||||
|
value: O("initial")
|
||||||
|
}, {
|
||||||
|
set(key, value){
|
||||||
|
const p= this.value[key] || O();
|
||||||
|
p(value);
|
||||||
|
this.value[key]= p;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Array.from((new URL(location)).searchParams.entries())
|
||||||
|
// .forEach(([ key, value ])=> O.action(store, "set", key, value));
|
||||||
|
// O.on(store, data=> history.replaceState("", "", "?"+(new URLSearchParams(JSON.parse(JSON.stringify(data)))).toString()));
|
||||||
|
useAdapter(store, store_adapter, {
|
||||||
|
onread(data){
|
||||||
|
Array.from(data.entries())
|
||||||
|
.forEach(([ key, value ])=> O.action(store, "set", key, value));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return el("input", {
|
||||||
|
className,
|
||||||
|
value: store().value(),
|
||||||
|
type: "text",
|
||||||
|
onchange: ev=> O.action(store, "set", "value", ev.target.value)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function useAdapter(observable, adapter, { onread, onbeforewrite }= {}){
|
||||||
|
if(!onread) onread= observable;
|
||||||
|
if(!onbeforewrite) onbeforewrite= data=> JSON.parse(JSON.stringify(data));
|
||||||
|
onread(adapter.read()); //TODO OK as synchronous
|
||||||
|
O.on(observable, data=> adapter.write(onbeforewrite(data)));
|
||||||
|
}
|
@ -3,10 +3,12 @@ document.head.append(style.element);
|
|||||||
import { fullNameComponent } from './components/fullNameComponent.js';
|
import { fullNameComponent } from './components/fullNameComponent.js';
|
||||||
import { todosComponent } from './components/todosComponent.js';
|
import { todosComponent } from './components/todosComponent.js';
|
||||||
import { CustomHTMLTestElement } from "./components/webComponent.js";
|
import { CustomHTMLTestElement } from "./components/webComponent.js";
|
||||||
|
import { thirdParty } from "./components/3rd-party.js";
|
||||||
|
|
||||||
document.body.append(
|
document.body.append(
|
||||||
el("h1", "Experiments:"),
|
el("h1", "Experiments:"),
|
||||||
el(fullNameComponent),
|
el(fullNameComponent),
|
||||||
el(todosComponent),
|
el(todosComponent),
|
||||||
el(CustomHTMLTestElement.tagName, { name: "attr" })
|
el(CustomHTMLTestElement.tagName, { name: "attr" }),
|
||||||
|
el(thirdParty)
|
||||||
);
|
);
|
||||||
|
27
index.d.ts
vendored
27
index.d.ts
vendored
@ -48,31 +48,24 @@ export function classListDeclarative<El extends SupportedElement>(element: El, c
|
|||||||
export function assign<El extends SupportedElement>(element: El, ...attrs_array: ElementAttributes<El>[]): El
|
export function assign<El extends SupportedElement>(element: El, ...attrs_array: ElementAttributes<El>[]): El
|
||||||
export function assignAttribute<El extends SupportedElement, ATT extends keyof ElementAttributes<El>>(element: El, attr: ATT, value: ElementAttributes<El>[ATT]): ElementAttributes<El>[ATT]
|
export function assignAttribute<El extends SupportedElement, ATT extends keyof ElementAttributes<El>>(element: El, attr: ATT, value: ElementAttributes<El>[ATT]): ElementAttributes<El>[ATT]
|
||||||
|
|
||||||
type ExtendedHTMLElementTagNameMap= ddeHTMLElementTagNameMap & CustomElementTagNameMap & ddePublicElementTagNameMap
|
type ExtendedHTMLElementTagNameMap= HTMLElementTagNameMap & CustomElementTagNameMap & ddePublicElementTagNameMap
|
||||||
export function el<
|
export function el<
|
||||||
TAG extends string,
|
TAG extends keyof ExtendedHTMLElementTagNameMap & string,
|
||||||
EL extends (TAG extends keyof ExtendedHTMLElementTagNameMap ? ExtendedHTMLElementTagNameMap[TAG] : ddeHTMLElement)
|
EL extends (TAG extends keyof ExtendedHTMLElementTagNameMap ? ExtendedHTMLElementTagNameMap[TAG] : HTMLElement)
|
||||||
>(
|
>(
|
||||||
tag_name: TAG,
|
tag_name: TAG,
|
||||||
attrs?: ElementAttributes<EL>,
|
attrs?: string | Observable<string, any> | ElementAttributes<EL>,
|
||||||
...addons: ddeElementAddon<TAG extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[TAG] : EL>[]
|
...addons: ddeElementAddon<EL>[]
|
||||||
): EL
|
): TAG extends keyof ddeHTMLElementTagNameMap ? ddeHTMLElementTagNameMap[TAG] : ddeHTMLElement
|
||||||
export function el<
|
export function el(
|
||||||
TAG extends string,
|
|
||||||
EL extends (TAG extends keyof ExtendedHTMLElementTagNameMap ? ExtendedHTMLElementTagNameMap[TAG] : ddeHTMLElement)
|
|
||||||
>(
|
|
||||||
tag_name: TAG,
|
|
||||||
attrs?: string | Observable<string, any>,
|
|
||||||
...addons: ddeElementAddon<TAG extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[TAG] : EL>[]
|
|
||||||
): EL
|
|
||||||
export function el<T>(
|
|
||||||
tag_name?: "<>",
|
tag_name?: "<>",
|
||||||
): ddeDocumentFragment
|
): ddeDocumentFragment
|
||||||
|
|
||||||
export function el<
|
export function el<
|
||||||
A extends ddeComponentAttributes,
|
A extends ddeComponentAttributes,
|
||||||
C extends (attr: Partial<A>)=> SupportedElement | DocumentFragment>(
|
C extends (attr: Partial<A>)=> SupportedElement | DocumentFragment
|
||||||
fComponent: C,
|
>(
|
||||||
|
component: C,
|
||||||
attrs?: A | string,
|
attrs?: A | string,
|
||||||
...addons: ddeElementAddon<ReturnType<C>>[]
|
...addons: ddeElementAddon<ReturnType<C>>[]
|
||||||
): ReturnType<C>
|
): ReturnType<C>
|
||||||
|
1076
package-lock.json
generated
1076
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
37
package.json
37
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "deka-dom-el",
|
"name": "deka-dom-el",
|
||||||
"version": "0.7.2",
|
"version": "0.7.3",
|
||||||
"description": "A low-code library that simplifies the creation of native DOM elements/components using small wrappers and tweaks.",
|
"description": "A low-code library that simplifies the creation of native DOM elements/components using small wrappers and tweaks.",
|
||||||
"author": "Jan Andrle <andrle.jan@centrum.cz>",
|
"author": "Jan Andrle <andrle.jan@centrum.cz>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -20,17 +20,17 @@
|
|||||||
"import": "./index.js",
|
"import": "./index.js",
|
||||||
"types": "./index.d.ts"
|
"types": "./index.d.ts"
|
||||||
},
|
},
|
||||||
"./jsdom": {
|
|
||||||
"import": "./jsdom.js",
|
|
||||||
"types": "./jsdom.d.ts"
|
|
||||||
},
|
|
||||||
"./observables": {
|
"./observables": {
|
||||||
"import": "./observables.js",
|
"import": "./observables.js",
|
||||||
"types": "./observables.d.ts"
|
"types": "./observables.d.ts"
|
||||||
},
|
},
|
||||||
|
"./jsdom": {
|
||||||
|
"import": "./jsdom.js",
|
||||||
|
"types": "./jsdom.d.ts"
|
||||||
|
},
|
||||||
"./src/observables-lib": {
|
"./src/observables-lib": {
|
||||||
"import": "./src/observables-lib.js",
|
"import": "./src/observables-lib.js",
|
||||||
"types": "./src/observables.d.ts"
|
"types": "./src/observables-lib.d.ts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
@ -59,27 +59,32 @@
|
|||||||
{
|
{
|
||||||
"path": "./index.js",
|
"path": "./index.js",
|
||||||
"limit": "9 kB",
|
"limit": "9 kB",
|
||||||
"gzip": false
|
"gzip": false,
|
||||||
|
"brotli": false
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "./observables.js",
|
"path": "./observables.js",
|
||||||
"limit": "11.5 kB",
|
"limit": "11.5 kB",
|
||||||
"gzip": false
|
"gzip": false,
|
||||||
|
"brotli": false
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "./jsdom.js",
|
"path": "./jsdom.js",
|
||||||
"limit": "10 kB",
|
"limit": "10 kB",
|
||||||
"gzip": false
|
"gzip": false,
|
||||||
|
"brotli": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "./examples/components/webComponent.js",
|
"path": "./examples/components/webComponent.js",
|
||||||
"limit": "13 kB",
|
"limit": "13 kB",
|
||||||
"gzip": false
|
"gzip": false,
|
||||||
|
"brotli": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "./examples/components/webComponent.js",
|
"path": "./examples/components/webComponent.js",
|
||||||
"limit": "5 kB",
|
"limit": "5 kB"
|
||||||
"gzip": true
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modifyEsbuildConfig": {
|
"modifyEsbuildConfig": {
|
||||||
@ -95,12 +100,12 @@
|
|||||||
"typescript"
|
"typescript"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@size-limit/preset-small-lib": "^8.2.6",
|
"@size-limit/preset-small-lib": "^11.0.1",
|
||||||
"dts-bundler": "^0.1.0",
|
"dts-bundler": "^0.1.0",
|
||||||
"esbuild": "^0.19.2",
|
"esbuild": "^0.19.9",
|
||||||
"jsdom": "^22.1.0",
|
"jsdom": "^23.0.1",
|
||||||
"jshint": "^2.13.6",
|
"jshint": "^2.13.6",
|
||||||
"nodejsscript": "github:jaandrle/nodejsscript#dev-v1",
|
"nodejsscript": "github:jaandrle/nodejsscript#dev-v1",
|
||||||
"size-limit-node-esbuild": "^0.2.0"
|
"size-limit-node-esbuild": "^0.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
4
src/observables-lib.d.ts
vendored
Normal file
4
src/observables-lib.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import type { Action, Actions, observable as o, Observable, SymbolOnclear } from "../observables.d.ts";
|
||||||
|
export { Action, Actions, Observable, SymbolOnclear };
|
||||||
|
export const O: o;
|
||||||
|
export const observable: o;
|
Loading…
Reference in New Issue
Block a user