Package Exports
- web-ui-pack
- web-ui-pack/index.js
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (web-ui-pack) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
web-ui-pack
Web package with high scalable WebComponents and helpers
Demo
You can see demo here or just clone repo and run npm i & npm start
Features
- Possible to use with/without any frameworks like Angular, React, Vue etc. (because it's js-native logic)
- Form/controls are ready to use and has built-in completed validation logic for any case that you can imagine (see demo/controls)
- Focus on accessibility (best practices), other packages has low-accessibility support
- High scalable and easy customizable (every component is developed to easy inherit and redefine/extend default logic)
- Built-in css-variables to use custom color-themes with native ordinary styling (css, scss etc.)
- Built-in Typescript (coverage types 100%)
- Built-in
.jsx/.tsx
support (for React/Vue) - Well documented via JSDoc (use intellisense power of your editor to get details about each property/option/usage)
- Optimized for webpack (build includes only used components and helpers via side-effects option)
- Zero dependancy (don't need to wait for bug-fixing of other packages)
- Always 100% test coverage via e2e and unit tests (it's must-have and always will be so)
- Focus on performance (it's important to have low-memory consumption and fastest initialization)
Why the package is so big
It's developed with Typescript and has huge built-in documentation (JSDoc). Every method,property,event is documented well so you don't need extra resource to take an example to implement or configure elements. In build-result without comments you will see that it's small-enough
Installing
Using npm:
npm install web-ui-pack
TODO
- Helpers
- Helper.Observer
- PopupElement demo
- SpinElement demo
- FormElement demo
- TextControl demo
- PasswordControl demo
- SwitchControl (Toggler) demo
- CheckControl (Checkbox) demo
- RadioControl (RadioGroup) demo
- SelectControl (ComboBox, Dropdown) demo
- Calendar
- DateControl
- SelectManyControl (MultiSelect)
- TextareaControl
- NumberControl
- Mask/pattern for controls
- CheckTreeControl
- TimeControl ?
- DateTimeControl ?
- FileControl
- ImageControl (AvatarEditor)
- SearchControl ?
- ModalElement
- ConfirmModalElement
- FormModalElement
- InfiniteScroll
- VirtualScroll
- TableElement ?
Components
Common rules:
Naming
- All components named as
WUP..Element
,WUP..Control
and has<wup-...>
html-tags - Public properties/options/events/methods startsWith
$...
(events$onShow
,$onHide
, methods$show
,$hide
, props like$isOpen
etc.) - Every component/class has static
$defaults
(common options for current class) and personal$options
(per each component). See details in example $options
are observed. So changing options affects on component immediately after empty timeout (every component has staticobservedOptions
as set of watched options)
- All components named as
Usage
- For webpack sideEffects switched on (it's for optimization). But if you don't use webpack don't import from
web-ui-pack
directly (due to tree-shaking can be not smart enough). Instead useweb-ui-pack/path-to-element
- Every component has a good JSDoc so go ahead and read details directly during the coding
- Library compiled into ESNext. To avoid unexpected issues include this package into babel (use
exclude: /node_modules\/(?!(web-ui-pack)\/).*/
for babel-loader)
- For webpack sideEffects switched on (it's for optimization). But if you don't use webpack don't import from
Limitations
- In
jsx/tsx
instead ofclassName
useclass
attribute (React issue) - If you change custom html-attributes it will update
$options
, but if you change some option it removes related attribute (for performance reasons). Better to avoid usage attributes at all
- In
Inheritance
Components are developed to be easy customized and inherrited. Use ...$defaults of every class to configure behavior You can rewrite everything that you can imagine without digging a lot in a code. To be sure don't hesitate to take a look on *.d.ts or source code (there are enough comments to clarify even weird/difficult cases)
All Components inherrited from WUPBaseElement that extends default HTMLElement
All internal event-callbacks startsWith
got...
(gotReady, gotRemoved)To redefine component just extend it and register with new html tag OR redefine default behavior via prototype functions (if $defaults are not included something). See details in example
Inherritance Tree
Example
Check how you can use every element/control (popupElement for example)
Typescript
import WUPPopupElement, { ShowCases } from "web-ui-pack/popup/popupElement";
// redefine some defaults; WARN: you can change placement rules here without changing $options per each element!!!
WUPPopupElement.$defaults.offset = [2, 2];
WUPPopupElement.$defaults.minWidthByTarget = true;
WUPPopupElement.$defaults.arrowEnable = true;
// create element
const el = document.createElement("wup-popup");
// WARN el.$options is a observable-clone of WUPPopupElement.$defaults
// WARN: ShowCases is const enum and import ShowCases available only in Typescript
el.$options.showCase = ShowCases.onClick | ShowCases.onFocus; // show popup by target.click and/or target.focus events
el.$options.target = document.querySelector("button");
/*
Placement can be $top, $right, $bottom, $left (top - above at the target etc.)
every placement has align options: $start, $middle, $end (left - to align at start of target)
also you can set $adjust to allow Reduce popup to fit layout
*/
el.$options.placement = [
WUPPopupElement.$placements.$top.$middle; // place at the top of target and align by vertical line
WUPPopupElement.$placements.$bottom.$middle.$adjust, // adjust means 'ignore align to fit layout`
WUPPopupElement.$placements.$bottom.$middle.$adjust.$resizeHeight, // resize means 'allow to resize to fit layout'
]
document.body.append(el);
HTML, JSX, TSX
<button id="btn1">Target</button>
<!-- You can skip pointing attribute 'target' if popup appended after target -->
<wup-popup target="#btn1" placement="top-start">Some content here</wup-popup>
How to extend/override
/// popup.ts
// you can override via prototypes
const original = WUPPopupElement.prototype.goShow;
WUPPopupElement.prototype.goShow = function customGoShow() {
if (window.isBusy) {
return null;
}
return original(...arguments);
};
/*** OR create extended class ***/
class Popup extends WUPPopupElement {
// take a look on definition of WUPPopupElement and you will find internals
protected override goShow(showCase: WUPPopup.ShowCases): boolean {
if (showCase === WUPPopup.ShowCases.onHover) {
return false;
}
return super.goShow(showCase);
}
}
const tagName = "ext-popup";
customElements.define(tagName, Popup);
// That's it. New Popup with custom tag 'ext-popup' is ready
// add for intellisense (for *.ts only)
declare global {
// add element to document.createElement
interface HTMLElementTagNameMap {
[tagName]: Popup;
}
// add element for tsx/jsx intellisense
namespace JSX {
interface IntrinsicElements {
[tagName]: IntrinsicElements["wup-popup"];
}
}
}
Helpers
use import focusFirst from "web-ui-pack/helpers/focusFirst"
etc.
WARN: don't use import {focusFirst} from "web-ui-pack;
because in this case the whole web-ui-pack module traps in compilation of dev-bundle and increases time of compilation
- animateDropdown(el: HTMLElement, timeMs=300, isClose=false) ⇒
Animate (open/close) element as dropdown via scale and counter-scale for children
- findScrollParent(el: HTMLElement) ⇒
Find first parent with active scroll X/Y
- findScrollParentAll(e: HTMLElement) ⇒
Find all parents with active scroll X/Y
- focusFirst(el: HTMLElement) ⇒
Set focus on element or first possible nested element
- isIntoView(el: HTMLElement) ⇒
Check if element is visible in scrollable parents
- nestedProperty.set ⇒
nestedProperty.set(obj, "value.nestedValue", 1) sets obj.value.nestedValue = 1
- nestedProperty.get ⇒
nestedProperty.get(obj, "nested.val2", out?: {hasProp?: boolean} ) returns value from obj.nested.val2
- objectClone(obj, opts: CloneOptions) ⇒
converts object to observable (via Proxy) to allow listen for changes
- observer ⇒
converts object to observable (via Proxy) to allow listen for changes
- onEvent(...args) ⇒
More strict (for Typescript) wrapper of addEventListener() that returns callback with removeListener()
- onFocusGot(el: HTMLElement, listener: (ev) => void, {debounceMs: 100, once: false, ...}) ⇒
Fires when element/children takes focus once (fires again after onFocusLost on element)
- onFocusLost(el: HTMLElement, listener: (ev) => void, {debounceMs: 100, once: false, ...}) ⇒
Fires when element/children completely lost focus
- onSpy(object: {}, method: string, listener: (...args) => void ⇒
Spy on method-call of object
- promiseWait(promise: Promise, ms: number, smartOrCallback: boolean | Function) => Promise ⇒
Produce Promise during for "no less than pointed time"; it helps for avoding spinner blinking during the very fast api-request in case: pending > waitResponse > resetPending
- scrollIntoView(el: HTMLElement, options: WUPScrollOptions) => Promise ⇒
Scroll the HTMLElement's parent container such that the element is visible to the user and return promise by animation end
- stringLowerCount(text: string, stopWith?: number) ⇒
Returns count of chars in lower case (for any language with ignoring numbers, symbols)
- stringUpperCount(text: string, stopWith?: number) ⇒
Returns count of chars in upper case (for any language with ignoring numbers, symbols)
- stringPrettify(text: string, changeKebabCase = false) ⇒
Changes camelCase, snakeCase, kebaCase text to user-friendly
Helpers.Observer
import observer from "web-ui-pack/helpers/observer";
const rawNestedObj = { val: 1 };
const raw = { date: new Date(), period: 3, nestedObj: rawNestedObj, arr: ["a"] };
const obj = observer.make(raw);
const removeListener = observer.onPropChanged(obj, (e) => console.warn("prop changed", e)); // calls per each changing
const removeListener2 = observer.onChanged(obj, (e) => console.warn("object changed", e)); // calls once after changing of bunch props
obj.period = 5; // fire onPropChanged
obj.date.setHours(0, 0, 0, 0); // fire onPropChanged
obj.nestedObj.val = 2; // fire onPropChanged
obj.arr.push("b"); // fire onPropChanged
obj.nestedObj = rawNestedObj; // fire onPropChanged
obj.nestedObj = rawNestedObj; // WARNING: it fire events again because rawNestedObj !== obj.nestedObj
removeListener(); // unsubscribe
removeListener2(); // unsubscribe
// before timeout will be fired onChanged (single time)
setTimeout(() => {
console.warn("WARNING: raw vs observable", {
equal: raw === obj,
equalByValueOf: raw.valueOf() === obj.valueOf(),
isRawObserved: observer.isObserved(raw),
isObjObserved: observer.isObserved(obj),
});
// because after assigning to observable it converted to observable also
console.warn("WARNING: rawNestedObj vs observable", {
equal: rawNestedObj === obj.nestedObj,
equalByValueOf: rawNestedObj.valueOf() === obj.nestedObj.valueOf(),
isRawObserved: observer.isObserved(rawNestedObj),
isNestedObserved: observer.isObserved(obj.nestedObj),
});
});
Troubleshooting & Rules
- Every object assigned to
observed
is converted toobserved
also - When you change array in most cases you get changing
length
; alsosort
/reverse
triggers events - WeakMap, WeakSet, HTMLElement are not supported (not observed)
- All objects compares by
valueOf()
so you maybe interested in custom valueOf to avoid unexpected issues
Troubleshooting
Be sure that you familiar with common rules
Library doesn't work in some browsers
web-ui-pack is compiled to ESNext. So some features maybe don't exist in browsers. To resolve it include the lib into babel-loader (for webpack check module.rules...exclude sections
// webpack.config.js { test: /\.(js|jsx)$/, exclude: (() => { // these packages must be included to change according to browserslist const include = ["web-ui-pack"]; return (v) => v.includes("node_modules") && !include.some((lib) => v.includes(lib)); })(), use: [ "babel-loader", ], },
UI doesn't recognize html tags like <wup-popup />
etc
It's possible if you missed import or it was removed by optimizer of wepback etc. To fix this you need to force import at least once
import { WUPSelectControl, WUPTextControl } from "web-ui-pack"; // this force webpack not ignore imports (if imported used only as html-tags without direct access) const sideEffect = WUPTextControl && WUPSelectControl; !sideEffect && console.error("Missed"); // It's required otherwise import is ignored by webpack // or WUPTextControl.$defaults.validateDebounceMs = 500; WUPSelectControl.$defaults.validateDebounceMs = 500; // etc.