cotomy
v1.0.1
Published
> This library targets ES2020+. > For older browsers (e.g. iOS 13 or IE), you will need a Polyfill such as `core-js`.
Readme
Cotomy
This library targets ES2020+.
For older browsers (e.g. iOS 13 or IE), you will need a Polyfill such ascore-js.
Cotomy is a lightweight framework for managing form behavior and page controllers in web applications.
It is suitable for both SPAs (Single Page Applications) and traditional web apps requiring dynamic form operations.
To install Cotomy in your project, run the following command:
npm i cotomyUsage
Cotomy will continue to expand with more detailed usage instructions and code examples added to the README in the future.
For the latest updates, please check the official documentation or repository regularly.
Reference: https://cotomy.net/
View Reference
The View layer provides thin wrappers around DOM elements and window events.
CotomyElement— A wrapper aroundHTMLElementwith convenient utilities for scoped CSS, querying, attributes/styles, geometry, and event handling.CotomyMetaElement— Convenience wrapper for<meta>tags.CotomyWindow— A singleton that exposes window-level events and helpers.
CotomyElement
- Constructor
new CotomyElement(element: HTMLElement)new CotomyElement(html: string)— Creates an element from HTML (single root required)new CotomyElement({ html, css? })— Creates from HTML and injects scoped CSSnew CotomyElement({ tagname, text?, css? })
- Scoped CSS
scopeId: string- Returns the value stored in the element'sdata-cotomy-scopeidattribute[root]placeholder in provided CSS is replaced by[data-cotomy-scopeid="..."]- If no
[root]is present,[root]is treated as if it were prefixed automatically - Scoped CSS text is kept on the instance; if the
<style id="css-${scopeId}">is missing when the element is attached, it will be re-generated automatically stylable: boolean- False for tags likescript,style,link,meta
- Static helpers
CotomyElement.encodeHtml(text)CotomyElement.first(selector, type?)CotomyElement.last(selector, type?)CotomyElement.find(selector, type?)CotomyElement.contains(selector)/CotomyElement.containsById(id)CotomyElement.byId(id, type?)CotomyElement.empty(type?)— Creates a hidden placeholder element
- Identity & matching
instanceId: string— Always returns the element'sdata-cotomy-instance(generated when missing, respects existing values)id: string | null | undefinedgenerateId(prefix = "__cotomy_elem__"): thisis(selector: string): boolean— Parent-aware matching helperempty: boolean— True for tags that cannot have children or have no content
- Attributes, classes, styles
attribute(name)/attribute(name, value | null | undefined): this—null/undefinedremoves the attributehasAttribute(name): booleanaddClass(name): this/removeClass(name): this/toggleClass(name, force?): this/hasClass(name): booleanstyle(name)/style(name, value | null | undefined): this—null/undefinedremoves the style
- Content & value
text: string(get/set)html: string(get/set)value: string— Works for inputs; falls back todata-cotomy-valueotherwisereadonly: boolean(get/set) — Uses native property if available, otherwise attributedisabled: boolean(get/set) — Uses native property if available (respectsfieldset[disabled]), otherwise attributeenabled: boolean(get/set) — TogglesdisabledattributesetFocus(): void
- Tree traversal & manipulation
parent: CotomyElementparents: CotomyElement[]children(selector = "*", type?): T[](direct children only)firstChild(selector = "*", type?)lastChild(selector = "*", type?)closest(selector, type?)find(selector, type?)/first(selector = "*", type?)/last(selector = "*", type?)/contains(selector)append(child): this/prepend(child): this/appendAll(children): this— acceptsCotomyElement, HTML string, or{ html, css? }insertBefore(sibling): this/insertAfter(sibling): this— acceptsCotomyElement, HTML string, or{ html, css? }appendTo(target): this/prependTo(target): thiscomesBefore(target): boolean/comesAfter(target): boolean— Checks DOM order (returnsfalsefor the same element or disconnected nodes)clone(type?): CotomyElement- Returns a deep-cloned element, optionally typed, and reassigns a newdata-cotomy-instancewhile preserving thedata-cotomy-scopeidfor scoped CSS sharing (stripsdata-cotomy-moving). Cloning an invalidated element (data-cotomy-invalidated) throws.clear(): this— Removes all descendants and textremove(): void— Explicitly non-chainable after removal
- Geometry & visibility
visible: booleanwidth: number(get/set px)height: number(get/set px)innerWidth: number/innerHeight: numberouterWidth: number/outerHeight: number— Includes marginsscrollWidth: number/scrollHeight: number/scrollTop: numberscrollIn(options?: CotomyScrollOptions | Partial<CotomyScrollOptions>): this— Scrolls the nearest scrollable container (or window) to reveal the elementscrollTo(target, options?: CotomyScrollOptions | Partial<CotomyScrollOptions>): this— Convenience wrapper; iftargetis a selector it searches descendants then callsscrollIn()position(): { top, left }— Relative to viewportabsolutePosition(): { top, left }— Viewport + page scroll offsetscreenPosition(): { top, left }rect(): { top, left, width, height }innerRect()— Subtracts paddingoverlaps(target: CotomyElement): boolean— True if the two elements'rectvalues overlap (AABB)overlapElements: CotomyElement[]— Returns other CotomyElements (bydata-cotomy-instance) that overlap this element
- Events
- Generic:
on(eventOrEvents, handler, options?),off(eventOrEvents, handler?, options?),once(eventOrEvents, handler, options?),trigger(eventOrEvent[, Event])—eventOrEventsaccepts either a single event name or an array for batch registration/removal.triggeraccepts either an event name or a prebuiltEvent, emits bubbling events by default, and can be customized by passing anEvent. - Delegation:
onSubTree(eventOrEvents, selector, handler, options?)—eventOrEventscan also be an array for listening to multiple delegated events at once. - Mouse:
click,dblclick,mouseover,mouseout,mousedown,mouseup,mousemove,mouseenter,mouseleave - Keyboard:
keydown,keyup,keypress - Inputs:
change,input - Focus:
focus,blur,focusin,focusout - Viewport:
inview,outview(usesIntersectionObserver) - Layout (custom):
resize,scroll,changelayout— requireslistenLayoutEvents()on the element - Move lifecycle:
cotomy:transitstart,cotomy:transitend— emitted automatically byappend,prepend,insertBefore/After,appendTo, andprependTo. While moving, the element (and its descendants) receive a temporarydata-cotomy-movingattribute so removal observers know the node is still in transit. - Removal:
removed— fired when an element actually leaves the DOM (MutationObserver-backed). Becausecotomy:transitstart/transitendmanage thedata-cotomy-movingflag,removedonly runs for true detachments, making it safe for cleanup. - Registry isolation: イベントレジストリは
data-cotomy-instance(インスタンスID)単位で管理され、クローンは新しいインスタンスIDを持つため、同じscopeIdを共有していてもリスナーが混線しません。 - File:
filedrop(handler: (files: File[]) => void)
- Generic:
Example (scoped CSS and events):
import { CotomyElement } from "cotomy";
const panel = new CotomyElement({
html: `<div class="panel"><button class="ok">OK</button></div>`,
css: `
[root] .panel { padding: 8px; }
[root] .ok { color: green; }
`,
});
panel.onSubTree("click", ".ok", () => console.log("clicked!"));
document.body.appendChild(panel.element);Testing
Scoped CSS sharing/re-hydrationとインスタンス単位のイベント管理は tests/view.spec.ts に含まれています。主に確認したい場合は以下のコマンドで個別に実行できます:
npm test -- --run tests/view.spec.ts -t "constructs from multiple sources and applies scoped css"
npm test -- --run tests/view.spec.ts -t "preserves scope ids when cloning, including descendants"
npm test -- --run tests/view.spec.ts -t "regenerates instance ids and lifecycle hooks when cloning"
npm test -- --run tests/view.spec.ts -t "keeps event handlers isolated by instance even when sharing scope"
npm test -- --run tests/view.spec.ts -t "rehydrates scoped css when a clone shares scope after the original was removed"
npm test -- --run tests/view.spec.ts -t "strips moving flags when cloning"
npm test -- --run tests/view.spec.ts -t "throws when cloning an invalidated element"
npm test -- --run tests/view.spec.ts -t "compares document order with comesBefore/comesAfter"普段は npm test で全体を実行できます。上記のコマンドでは [root] 展開、スコープID共有のクローン挙動、インスタンス単位のイベント隔離、クローン後のスコープCSS再生成、移動フラグの除去、無効化要素のクローン拒否、DOM順序判定などをピンポイントで確認できます。
CotomyMetaElement
CotomyMetaElement.get(name): CotomyMetaElementcontent: string— Readscontentattribute.
CotomyWindow
- Singleton
CotomyWindow.instanceinitialized: boolean— Callinitialize()once after DOM is readyinitialize(): this
- DOM helpers
body: CotomyElementappend(element: CotomyElement | string | { html: string, css?: string }): thisscrollTo(target, options?: CotomyScrollOptions | Partial<CotomyScrollOptions>): this— Scrolls to reveal a target (selector | CotomyElement | HTMLElement)moveNext(focused: CotomyElement, shift = false)— Move focus to next/previous focusable
- Window events
on(eventOrEvents, handler): this/off(eventOrEvents, handler?): this/trigger(eventOrEvent[, Event]): this—eventOrEventsaccepts a single event name or an array. CotomyWindow’striggeraccepts an event name orEvent, bubbles by default, and accepts anEventto override the behavior.load(handler): this/ready(handler): thisresize([handler]): this/scroll([handler]): this/changeLayout([handler]): this/pageshow([handler]): this
- Window state
scrollTop,scrollLeft,width,height,documentWidth,documentHeightreload(): void(sets internalreloadingflag),reloading: boolean
Quick start:
import { CotomyWindow, CotomyElement } from "cotomy";
CotomyWindow.instance.initialize();
CotomyWindow.instance.ready(() => {
const el = new CotomyElement("<div>Hello</div>");
CotomyWindow.instance.append(el);
});Form Reference
The Form layer builds on CotomyElement for common form flows.
CotomyForm— Base class with submit lifecycle hooksCotomyQueryForm— Submits to query string (GET)CotomyApiForm— Submits viaCotomyApi(handlesFormData, errors, events)CotomyEntityApiForm— REST entity helper with surrogate key supportCotomyEntityFillApiForm— Adds automatic field filling and simple view binding
CotomyForm (base)
- Construction & basics
- Extends
CotomyElementand expects a<form>element initialize(): this— Wires asubmitlistener that callssubmitAsync()initialized: boolean— Set afterinitialize()submitAsync(): Promise<void>— Abstract in base
- Extends
- Routing & reload
method: string— Getter that defaults togetin base; specialized in subclassesactionUrl: string— Getter that defaults to theactionattribute or current pathreloadAsync(): Promise<void>— Page reload usingCotomyWindowautoReload: boolean— Backed bydata-cotomy-autoreload(default true)
CotomyQueryForm
- Always uses
GET submitAsync()merges current query string with form inputs and navigates vialocation.href.
CotomyApiForm
- API integration
apiClient(): CotomyApi— Override to inject a client; default creates a new oneactionUrl: string— Usesactionattributemethod: string— Defaults topostformData(): FormData— Builds from form, convertsdatetime-localto ISO (UTC offset)submitAsync()— CallssubmitToApiAsync(formData)submitToApiAsync(formData): Promise<CotomyApiResponse>— UsesCotomyApi.submitAsync
- Events
apiFailed(handler)— Listens tocotomy:apifailedsubmitFailed(handler)— Listens tocotomy:submitfailed- Both events bubble from the form element; payload is
CotomyApiFailedEvent
CotomyEntityApiForm
- Surrogate key flow
data-cotomy-entity-key— Holds the entity identifier if presentdata-cotomy-identify— Defaults to true; when true and201 Createdis returned, the form extracts the key fromLocationand stores it indata-cotomy-entity-keyactionUrl— Appends the key to the baseactionwhen present; otherwise normalizes trailing slash for collection URLmethod—putwhen key exists; otherwisepost(unlessmethodattribute is explicitly set)
entityKey: string | undefined— Read-only accessor for the currentdata-cotomy-entity-keyvalue
CotomyEntityFillApiForm
- Data loading and field filling
initialize()— Adds default fillers and triggersloadAsync()onCotomyWindow.readyreloadAsync()— Alias toloadAsync()loadAsync(): Promise<CotomyApiResponse>— CallsCotomyApi.getAsyncwhencanLoadis trueloadActionUrl: string— Defaults toactionUrl; override or set for custom endpointscanLoad: boolean— Defaults tohasEntityKey
- Naming & binding
bindNameGenerator(): ICotomyBindNameGenerator— Defaults toCotomyBracketBindNameGenerator(user[name])renderer(): CotomyViewRenderer— Applies[data-cotomy-bind]to view elements
filler(type, (input, value))— Register fillers; defaults provided fordatetime-local,checkbox,radio- Fills non-array, non-object fields by matching input/select/textarea
name
- Fills non-array, non-object fields by matching input/select/textarea
View binding renderers
CotomyViewRenderer includes a few built-in helpers for [data-cotomy-bindtype]:
mail,tel,url— Wrap the value in a corresponding anchor tag.number— UsesIntl.NumberFormatwithdata-cotomy-locale/data-cotomy-currencyinheritance.data-cotomy-fraction-digits="2"— Forces fixed fraction digits (sets bothminimumFractionDigitsandmaximumFractionDigits). Works with or withoutdata-cotomy-currency(e.g.0→0.00).
utc— Treats the value as UTC (or appendsZwhen missing) and formats withdata-cotomy-format(defaultYYYY/MM/DD HH:mm).date— Renders local dates withdata-cotomy-format(defaultYYYY/MM/DD) when the input is a validDatevalue.
Example:
const view = new CotomyViewRenderer(
new CotomyElement(document.querySelector("#profile")!),
new CotomyBracketBindNameGenerator()
);
await view.applyAsync(apiResponse); // apiResponse is CotomyApiResponse from CotomyApi
// <span data-cotomy-bind="user.birthday" data-cotomy-bindtype="date" data-cotomy-format="MMM D, YYYY"></span>
// → renders localized date text if the API payload contains user.birthdayArray binding
- Both
CotomyViewRenderer.applyAsyncandCotomyEntityFillApiForm.fillAsyncresolve array elements by index via the activeICotomyBindNameGenerator(dot style →items[0].name, bracket style →items[0][name]). - Cotomy does not create or clone templates for you. Prepare the necessary DOM (e.g., table rows, list items, individual inputs) ahead of time, then call
fillAsync/applyAsyncto populate the values. - Primitive arrays (strings, numbers, booleans, etc.) are treated the same way—have matching
[data-cotomy-bind]/nameattributes ready for every index you want to show. - If you need dynamic row counts, generate the markup yourself before invoking Cotomy; the framework purposely avoids mutating the structure so it does not get in your way.
Example:
import { CotomyEntityFillApiForm } from "cotomy";
const form = new CotomyEntityFillApiForm(document.querySelector("form")!);
form.initialize();
form.apiFailed(e => console.error("API failed", e.response.status));
form.submitFailed(e => console.warn("Submit failed", e.response.status));Entity API forms
CotomyEntityApiForm targets REST endpoints that identify records with a single surrogate key.
Attach data-cotomy-entity-key="<id>" to the form when editing an existing entity; omit the attribute (or leave it empty) to issue a POST to the base action URL.
On 201 Created, the form reads the Location header and stores the generated key back into data-cotomy-entity-key, enabling subsequent PUT submissions.
Composite or natural keys are no longer supported—migrate any legacy markup that relied on data-cotomy-keyindex or multiple key inputs to the new surrogate-key flow.
When you must integrate with endpoints that still expect natural identifiers, subclass CotomyEntityApiForm/CotomyEntityFillApiForm, override canLoad to supply your own load condition, and adjust loadActionUrl (plus any submission hooks) to build the appropriate URL fragments.
The core of Cotomy is CotomyElement, which is constructed as a wrapper for Element.
By passing HTML and CSS strings to the constructor, it is possible to generate Element designs with a limited scope.
const ce = new CotomyElement({
html: /* html */`
<div>
<p>Text</p>
</div>
`,
css: /* css */`
[root] {
display: block;
}
[root] > p {
text-align: center;
}
`
});"display HTML in character literals with color coding"→"syntax highlighting for embedded HTML""generate Element designs with a limited scope"→"generate scoped DOM elements with associated styles"
Development
Cotomy ships with both ESM (dist/esm) and CommonJS (dist/cjs) builds, plus generated type definitions in dist/types.
For direct <script> usage, browser-ready bundles are available at dist/browser/cotomy.js and dist/browser/cotomy.min.js (also served via the npm unpkg entry).
Include the minified build like so:
<script src="https://unpkg.com/cotomy/dist/browser/cotomy.min.js"></script>
<script>
const el = new Cotomy.CotomyElement("<div>Hello</div>");
document.body.appendChild(el.element);
</script>Run the build to refresh every target bundle:
npm install
npm run buildThe Vitest-based test suite can be executed via:
npx vitest runLicense
This project is licensed under the MIT License.
Contact
You can reach out to me at: [email protected]
GitHub repository: https://github.com/yshr1920/cotomy
