Browser-first web utilities library— small, mostly-static-method namespaces over DOM elements/nodes, events-as-Observables, storage/cookies, canvas/clipboard/download conversions, an HTML-auto-parsing fetcher, script loading, a reactiveRouterfamily, dynamic CSS, animation-frame FPS helpers, and browser/platform detection. Built on top of@dooboostore/core'sObservable/Subjectreactive primitives.
ElementUtils,NodeUtils,CanvasUtils,DocumentUtilsfor low-level element/node/canvas operationsEventUtils/WindowUtilsconvertaddEventListenerinto@dooboostore/coreObservablestreamsStorageUtils(Local/Session storage, explicitwindowper call) andCookieUtilsConvertUtils(Canvas/ImageBitmap/File/Blob ⇄ Base64/ArrayBuffer, image compression),ClipBoardUtils,DownloadUtilsHttpDomParserFetcher, anHttpFetcher(from@dooboostore/core) subclass that auto-parses HTML/XML/SVG responses into aDocumentScriptUtilsfor injecting<script>/<link>tags and evaluating script stringsRouterfamily (PathRouter,HashRouter,LocationRouter) backed by aBehaviorSubjectof route changesStyleCssUtilsfor computed styles and CSS custom propertiesAnimationFrameUtilsfor FPS measurement overrequestAnimationFrameValidUtilsfor browser/webview/mobile checks@dooboostore/coreis the only runtime dependency (reflect-metadatais a peer dependency)npm install @dooboostore/core-web# orpnpm add @dooboostore/core-webAll exports are available from the package root (import { ... } from '@dooboostore/core-web'); the only otherexportsentry is./bundle-entry.
| Category | Exports | Real shape |
|---|---|---|
| DOM & Elements | ElementUtils,CanvasUtils,ElementApply | namespaces (ElementUtils/CanvasUtils), utility class (ElementApply) |
| Node tree | NodeUtils,NodeSlot,Slot | namespace + classes |
| Document | DocumentUtils | namespace, 2 functions |
| Events | EventUtils,WindowUtils | namespaces, 1 function each |
| Storage | StorageUtils,CookieUtils | class with static methods / namespace |
| Conversion | ConvertUtils,ClipBoardUtils,DownloadUtils | namespaces |
| Network | HttpDomParserFetcher | class (extends@dooboostore/core'sHttpFetcher) |
| Script loading | ScriptUtils | class with static methods |
| Images | ImageUtils | namespace —currently empty, no exported members |
| Navigation | LocationUtils,Router,PathRouter,HashRouter,LocationRouter | namespace + reactive class hierarchy |
| Styling | StyleCssUtils | namespace |
| Animation | AnimationFrameUtils | namespace |
| Platform detection | ValidUtils | namespace |
import{ElementUtils}from'@dooboostore/core-web';ElementUtils.removeAllChildren(el);// clear all childrenElementUtils.removeAllChildren(el, newChild);// clear then insert one or many// Promise-based image/audio preloadingconstimg =awaitElementUtils.loadImage('/photo.jpg');ElementUtils.loadImage('/photo.jpg', {onload:img=>console.log(img),onerror:e=>console.error(e) });constaudio =awaitElementUtils.loadAudio('/clip.mp3');// Fragment <-> HTML stringconstfragment =ElementUtils.htmlToFragment('<b>Hi</b>');consthtml =ElementUtils.toInnerHTML(fragment, {document});ElementUtils.replaceWith(oldEl, newEl);constchildren =ElementUtils.nodeList(fragment);constcloned =ElementUtils.cloneNodeList(fragment);// querySelector/All also accept a { start, end } sibling range instead of an Elementconstone =ElementUtils.querySelector(container,'.item');constranged =ElementUtils.querySelectorAll({start: markerA,end: markerB },'.item');constels =ElementUtils.selectorElements('.item', container);// Element[] (root defaults to document)constnodes =ElementUtils.selectorNodes('.item');// raw NodeListconstattrsObj =ElementUtils.getAttributeToObject(el);// { [attrName]: value }conststyleObj =ElementUtils.getStyleToObject(el);// { [cssProp]: value }ElementUtils.setAttribute(el, ['disabled','hidden']);// sets each to ''ElementUtils.setAttributeAttr(el, [{name:'data-id',value:'42'}]);ElementUtils.removeAttribute(el, ['disabled']);import{CanvasUtils}from'@dooboostore/core-web';constctx = canvas.getContext('2d')!;CanvasUtils.Context2D.resetTransform(ctx);// ctx.setTransform(1,0,0,1,0,0)// Binary-search the largest font size that fits text in a box (no auto line-wrap)constsize =CanvasUtils.fontSize(ctx, {text:'Hello\nWorld',maxWidth:200,maxHeight:80});constmetrics =CanvasUtils.textMetrics(ctx,'Hello');ElementApplyis the comment-marker template patching engine (<!--ea:id:start...-->) that scans an element (and itsshadowRoot) and re-evaluates${...}-style bindings — this is the low-level primitive that@dooboostore/simple-web-component's@state/@applyNodedecorators are built on.NodeSlot/Slotare a similar comment-marker mechanism for named content regions (append/appendHtml/appendText/prepend/nodes), backing@dooboostore/simple-web-component's@applySlot. Both are exported for advanced/direct use, but are primarily meant to be consumed through that framework rather than used standalone.
import{NodeUtils}from'@dooboostore/core-web';NodeUtils.removeAllChildNode(node);NodeUtils.removeAllChildNodeAndAppend(node, newChildOrArray);NodeUtils.appendChild(parent, child);NodeUtils.replaceNode(oldNode, newNode);// replaces oldNode within its parentNodeUtils.insertAfter(targetNode, newNode);constclone =NodeUtils.cloneNode(el,true);// deep clone, typed// Walk a tree, deciding per-node whether to collect it and/or descend into its childrenconstmatches =NodeUtils.findNodes(root,node=>(nodeasElement).nodeName==='SPAN'?NodeUtils.FindNodesFilterResult.MATCH_AND_CONTINUE:NodeUtils.FindNodesFilterResult.NO_MATCH_AND_CONTINUE);import{DocumentUtils}from'@dooboostore/core-web';// Every element in the document/subtree that carries a given attribute, with its valueconstmarked =DocumentUtils.querySelectorAllByAttributeName(document,'data-track');// -> [{ element, value }, ...]// Note: despite the `document` parameter, this binds via window.addEventListener internallyconstscroll$ =DocumentUtils.eventObservable(document,'scroll');
scroll$.subscribe(e=>console.log('scrolled'));Both convert native DOM events into@dooboostore/coreObservables (subscribereturns aSubscriptionwith.unsubscribe()):
import{EventUtils,WindowUtils}from'@dooboostore/core-web';constclick$ =EventUtils.htmlElementEventObservable(button,'click');constsub = click$.subscribe(e=>console.log('clicked', e.clientX));
sub.unsubscribe();constresize$ =WindowUtils.eventObservable(window,'resize');
resize$.subscribe(() =>console.log('resized'));A static-method class — every method takes the targetwindowexplicitly (no implicit global, and no unifiedkey/valueAPI — Local and Session each have their own method names):
import{StorageUtils}from'@dooboostore/core-web';StorageUtils.setLocalStorageItem('theme','dark',window);// objects are JSON.stringify'd automaticallyconsttheme =StorageUtils.getLocalStorageItem('theme',window);// raw string backconstprefs =StorageUtils.getLocalStorageJsonItem<{fontSize:number}>('prefs',window);// JSON.parse'd, or undefinedStorageUtils.removeLocalStorageItem('theme',window);constcut =StorageUtils.cutLocalStorageItem('theme',window);// get then removeconstcutPrefs =StorageUtils.cutLocalStorageJsonItem<{fontSize:number}>('prefs',window);// JSON get then removeStorageUtils.clearLocalStorage(window);// Session-storage equivalents mirror every method above:// setSessionStorageItem / getSessionStorageItem / getSessionStorageJsonItem /// cutSessionStorageItem / cutSessionStorageJsonItem / removeSessionStorageItem / clearSessionStorageimport{CookieUtils}from'@dooboostore/core-web';CookieUtils.set('sessionId','12345', {maxAge:3600,path:'/',secure:true,sameSite:'Strict'});// expireSecond (not a Date) controls expiry: null clears it immediately, a number sets seconds-from-nowCookieUtils.set('token','abc', {expireSecond:7*24*60*60});constsessionId =CookieUtils.get('sessionId');// string | null, URI-decodedconstall =CookieUtils.names();// string[] of every cookie nameCookieUtils.remove('sessionId', {path:'/'});constcookieString =CookieUtils.make('k','v', {path:'/'});// build the raw "k=v; path=/" string without setting itCanvas/ImageBitmap/File/Blob conversions, keyed around a{ data, config }shape rather than one-function-per-pair:
import{ConvertUtils}from'@dooboostore/core-web';constconfig = {type:'image/png'asconst,quality:0.9};constblob =awaitConvertUtils.toBlob({data: canvas, config });// also accepts a File directly (passthrough)constbase64 =awaitConvertUtils.toBase64(blob, config);// accepts File/Blob/Image/ImageBitmap/Canvas (or a Promise of one)constfile =awaitConvertUtils.toFile(canvas, { ...config,filename:'shot.png'});constarrayBuffer =awaitConvertUtils.toArrayBuffer({data: canvas, config });constbitmap =awaitConvertUtils.toImageBitmap(file);constblobUrl =awaitConvertUtils.toBlobURL({data: canvas, config });// Downscale to a max width, either as a compressed Blob or a resized ImageBitmapconstcompressed =awaitConvertUtils.compressImage(bitmap, {maxWidth:800,type:'image/jpeg',quality:0.7});constcompressedBase64 =awaitConvertUtils.compressImageToBase64(bitmap, {maxWidth:800,type:'image/jpeg',quality:0.7});constobj =ConvertUtils.toObject<{name:string}>(formElement);// HTMLFormElement -> plain objectconstobjectUrl =ConvertUtils.toObjectUrl(file);// URL.createObjectURLconstdecoded =ConvertUtils.decodeHtmlEntity('&amp;',document);// Re-exports @dooboostore/core's own ConvertUtils under one namespaceConvertUtils.coreConvertUtils;import{ClipBoardUtils}from'@dooboostore/core-web';awaitClipBoardUtils.writeText('copied!',window);// falls back to a hidden textarea + execCommand('copy') if the Clipboard API is unavailableconsttext =awaitClipBoardUtils.readText(window);constitems =awaitClipBoardUtils.read(window);awaitClipBoardUtils.write(clipboardItems);// raw ClipboardItems, e.g. multiple MIME types at onceimport{DownloadUtils}from'@dooboostore/core-web';DownloadUtils.download(window, blob,'report.csv');// accepts a Blob or a plain URL stringDownloadUtils.download(window,'https://example.com/f.pdf','file.pdf');DownloadUtils.csvDownload(window, rows, {headers: ['id','name'],includeHeader:'true'});AnHttpFetcher<CONFIG, Document, PIPE>(from@dooboostore/core) subclass — instantiate it and call the inherited HTTP-verb methods; responses are parsed into aDocumentviaDOMParser, pickingtext/html,application/xml, orimage/svg+xmlfrom the response'scontent-type(override withforceParserType, or skip parsing entirely withbypassTransform: trueto get the rawResponse):
import{HttpDomParserFetcher}from'@dooboostore/core-web';constfetcher =newHttpDomParserFetcher();constdoc =awaitfetcher.get({target: {url:'https://example.com/page.html'} });console.log(doc.querySelector('title')?.textContent);Static methods that build and inject the tag themselves — you always pass thedocumentto inject into:
import{ScriptUtils}from'@dooboostore/core-web';awaitScriptUtils.loadScript(document,'https://cdn.example.com/lib.js', {async:'true'});awaitScriptUtils.loadStyleSheet(document,'/theme.css');awaitScriptUtils.loadScriptBody(document,'console.log("inline")');// Generic: create any tag with attributes/body; `created` decides where to insert itawaitScriptUtils.loadElement(document,'script', {attribute: {src:'/a.js'},created:el=>document.body.append(el) });// Evaluate a function body with `this` bound to thisTarget (thin wrappers over core's `ObjectUtils.Script`; both are marked deprecated)constsum =ScriptUtils.evaluate<number>('return this.a + this.b', {a:1,b:2});// 3constsame =ScriptUtils.evaluateReturn<number>('this.a + this.b', {a:1,b:2});// wraps in `return` for youexport namespace ImageUtils {}currently hasno exported members— the file only contains a commented-out draft. There is nothing to import from it yet.
Every function takes theWindowexplicitly (no implicit globalwindowaccess):
import{LocationUtils}from'@dooboostore/core-web';LocationUtils.path(window);// location.pathnameLocationUtils.search(window);// location.search, '' if noneLocationUtils.hash(window);// location.hash without the leading '#'LocationUtils.hashPath(window);// the part of the hash before '?'LocationUtils.hashSearch(window);// the '?...' part of the hash, '' if noneLocationUtils.pathQueryParams(window);// Map<string, string> from location.searchLocationUtils.pathQueryParamsObject(window);// same, as a plain objectLocationUtils.hashQueryParams(window);// Map<string, string> from the hash's queryLocationUtils.hashQueryParamsObject(window);// same, as a plain objectLocationUtils.queryStringToMap('a=1&b=2');LocationUtils.queryStringToObject('a=1&b=2');Router,PathRouter,HashRouter,LocationRouter)A reactive router: each instance exposes aBehaviorSubject-backed.observableof route changes (RouterEventType, withtriggerPoint: 'start' | 'end' | 'first-end') rather than a callback-registration table. All three concrete routers share the sameRouterAPI and differ only in how they read/write the URL (path segment,#hash, or a full navigation viaLocationRouter):
import{PathRouter}from'@dooboostore/core-web';constrouter =newPathRouter({window});
router.observable.subscribe(route=>{console.log(route.triggerPoint, route.path, route.searchParams);
});
router.push('/users/123');// history.pushStaterouter.replace('/dashboard');// history.replaceStaterouter.pushUpsertSearchParam({tab:'posts'});// merge into current search paramsrouter.pushDeleteSearchParam('tab');
router.go(-1);// history.go(-1) — or router.back() / router.forward()router.reload();
router.getPathName();// current pathnamerouter.getUrl();// pathname + searchrouter.searchParamObject;// current search params as a plain objectrouter.getPathData('/users/{id}');// extract path params against a pattern, or null if it doesn't matchHashRouterreads/writeslocation.hashinstead ofpathname(e.g. for static hosting without server-side routing);LocationRouterperforms a real navigation (location.href =/location.replace()) instead of using the History API.
Leave guards—addLeaveGuard((to, from) => boolean | Promise<boolean>)registers a guard and returns a function that removes it. Guards run beforego(...)navigates and on browser back/forward; anyfalsecancels (back/forward is undone by pushing the previous URL back). With no guards registered,go(...)stays synchronous as before.canLeave(to, from)evaluates the registered guards.
constoff = router.addLeaveGuard(async(to) => !(dirty && to.path!=='/edit'));// ...off();import{StyleCssUtils}from'@dooboostore/core-web';constvalue =StyleCssUtils.getPropertyValue('--primary-color', { el,window});StyleCssUtils.setPropertyValue('--primary-color','#6366f1', el);constcomputed =StyleCssUtils.getComputedStyle({ el,window});// Every custom property (--*) in effect on an element, or on :root + all same-origin stylesheets if no el is givenconstcssVars =StyleCssUtils.getPropertyEntries({window});// [[name, value], ...]import{AnimationFrameUtils}from'@dooboostore/core-web';constsub =AnimationFrameUtils.fps({window,second:1},({ fps }) =>{console.log('current fps:', fps.toFixed(1));
});
sub.unsubscribe();// stop the requestAnimationFrame loop// Throttle a per-frame callback to fire only `divideSize` times per second worth of framesAnimationFrameUtils.dividePerFpsObservable({fpsConfig: {window},divideSize:10})
.subscribe(({ fps, gapPosition, gapSize }) =>draw());import{ValidUtils}from'@dooboostore/core-web';ValidUtils.isBrowser();// typeof window/Window !== 'undefined'ValidUtils.isBrowser(win=>console.log(win));// also invokes callback with window if trueValidUtils.hasParentWindow(window);// running inside an iframe with a different parentValidUtils.isMobile();// navigator.userAgent test (/Mobi/i)ValidUtils.isiPhone();ValidUtils.isAndroidWebview();// heuristic: window.System bridge presentValidUtils.isWkWebview();// heuristic: webkit.messageHandlers.System presentValidUtils.isSocialNetworkServiceInappBrowser();// KakaoTalk/Naver/Facebook/Instagram in-app browser UA sniff// Re-exports @dooboostore/core's own ValidUtils under one namespaceValidUtils.coreValidUtils;import{EventUtils,StorageUtils}from'@dooboostore/core-web';letcount =StorageUtils.getLocalStorageJsonItem<number>('clickCount',window) ??0;constbutton =document.querySelector('#count-btn')!;constdisplay =document.querySelector('#count-display')!;
display.textContent=String(count);EventUtils.htmlElementEventObservable(button,'click').subscribe(() =>{
count++;
display.textContent=String(count);StorageUtils.setLocalStorageItem('clickCount', count,window);
});Observable/Subject/HttpFetcherand the other core utilities this package builds onElementApply,NodeSlot/Slot, and theRouterfamily from this packageHttpDomParserFetcher-style parsing in SSR contextsThis package is licensed under theMIT License.