@cocreate/elements
Bind any HTML element directly to your data using HTML5 attributes. An element declares where its value lives — a storage, database, array, document, and key — and the module handles the rest: reading the value in, writing changes back, and keeping it synchronized in realtime as other clients and servers broadcast updates. There is no query code, no fetch layer, and no view model. The markup is the data binding.
Documentation
For a complete guide and working demo refer to the documentation.
Table of Contents
- Features
- Installation
- Usage
- How it Works
- The Data Scope
- Core Attributes
- Behavior Attributes
- Key Expressions
- Value Pipeline
- Forms
- Filtering and Lists
- Realtime Synchronization
- Actions
- Element Methods
- JavaScript API
- Behavior Notes
- Announcements
- Roadmap
- How to Contribute
- About
- License
Features
- Declarative Data Binding: Point an element at a document field and it reads, renders, and persists that field without any application code.
- Automatic CRUD Resolution: The operation is inferred from the attributes present, so the same markup creates a record when it has no id and updates it when it does.
- Realtime Two-Way Sync: Elements subscribe to their own data scope and update instantly when the underlying record changes anywhere in the system.
- Batched Reads: Elements sharing a scope are collected into a single query, so a document bound across twenty fields performs one read.
- Forms as Documents: A form groups fields into one payload, propagates its scope to its children, and saves them as a single operation.
- Value Transformation Pipeline: Type coercion, prefixes, suffixes, replacements, casing, and encoding are applied on the way in and out.
- Filter Integration: Works with
@cocreate/filterto drive queries, sorting, pagination, and infinite scroll from markup. - Action Integration: Registers
save,delete, andresetso persistence composes into action chains. - Automatic Binding: Elements inserted later, including rendered templates, are initialized and subscribed automatically.
Installation
npm install @cocreate/elements
yarn add @cocreate/elements
CDN
<script src="https://cdn.CoCreateJS.com/elements/latest/CoCreate-elements.min.js"></script>
Usage
Display a Value
<span array="users" object="64b9a32e18f21bc56789abcd" key="name"></span>
The element reads the name field of that document and renders it. If the document changes anywhere in the system, the element updates.
Edit a Value
<input
array="users"
object="64b9a32e18f21bc56789abcd"
key="email"
realtime="true" />
With realtime, every keystroke persists the field. Without it, the value is saved when a save action runs.
Group Fields Into a Document
<form array="users" object="64b9a32e18f21bc56789abcd">
<input key="name" />
<input key="email" />
<textarea key="bio"></textarea>
<button actions="save">Save</button>
</form>
The form propagates its scope to every field, so each input only declares its key. Saving sends one payload containing all three fields.
Create a Record
<form array="users" object>
<input key="name" />
<input key="email" />
<button actions="save, reset">Create</button>
</form>
An empty object attribute means "no document yet". On save the module issues a create, then writes the new id back into the form and its fields, so subsequent saves update the same record.
Render a List
<div
array="users"
object
filter-query-key="status"
filter-query-value="active"
filter-limit="20"
filter-sort-key="name"
render-query="[template]">
<div template>
<span>{{object.name}}</span>
<span>{{object.email}}</span>
</div>
</div>
Delete a Record
<button
array="users"
object="64b9a32e18f21bc56789abcd"
actions="delete">
Delete
</button>
Multiple targets can be selected with delete-query. Each target declares its
own CRUD scope; object targets sharing the same storage, database, and array are
sent in one delete request.
<button
actions="delete"
delete-query="$document [data-delete]:checked[]">
Delete selected
</button>
<input
type="checkbox"
data-delete
array="users"
object="64b9a32e18f21bc56789abcd" />
How it Works
- On load the module scans for elements declaring
storage,database,array, orrender-json. - Each element's attributes are parsed into a normalized CRUD payload, and its type is inferred from which attributes are present.
- A dataKey is generated from the scope, and every element sharing that key is grouped together. Field-level
keyis deliberately excluded from document-scope keys so all fields of one document share a single scope. - The scope is subscribed to over the socket so future changes are pushed to every bound element.
- Reads are debounced and batched, so one query serves every element in the scope.
- The response is distributed to each element, which extracts its own field and renders it through the value pipeline.
- Editable elements listen for input. When
realtimeis enabled, changes persist automatically. - Writes resolve to a create or an update depending on whether an id is present, and the resulting id is propagated back into the markup.
The Data Scope
Attributes describe a hierarchy, from the storage provider down to a single field.
| Level | Attribute | Description |
|---|---|---|
| Storage | storage | Storage provider or alias |
| Database | database | Database name |
| Array | array | Collection, table, or dataset |
| Index | index | Index within the array |
| Object | object | Document id, empty for a new document |
| Key | key | Field within the document |
The type of operation is inferred from the deepest level present:
| Attributes Present | Inferred Type |
|---|---|
object or key (with array) | object |
index (with array) | index |
array | array |
database | database |
storage | storage |
Any of these may hold a comma separated list to target several scopes at once.
<div array="users, admins" key="name"></div>
Core Attributes
| Attribute | Description |
|---|---|
storage | Storage provider targeted by the operation |
database | Database name, defaults to the organization |
array | Collection or table name |
index | Index scope within the array |
object | Document id. Empty creates a new document, ObjectId() generates one, pending marks a create in flight |
key | Field path within the document |
updateName | Rename target when updating a named scope |
organization_id | Tenant identifier |
host | Host context |
apikey | API key used for the operation |
room | Realtime room scope |
reference | When false, strips storage metadata from the returned object |
Behavior Attributes
Each of these accepts "false" to opt out, and most default to enabled.
| Attribute | Description |
|---|---|
crud="false" | Excludes the element from binding entirely |
read="false" | Element never reads, it only writes |
save="false" | Element never writes, it only reads |
update="false" | Element ignores incoming updates after its first read |
listen="false" | Element ignores broadcasts, but still accepts its own reads |
delete | Marks the element as a delete target |
upsert | Creates the document if it does not exist when updating |
realtime="true" | Persists on every input event |
crdt="true" | Routes text edits through collaborative merge instead of overwriting |
broadcast="false" | Suppresses broadcasting the change to other clients |
broadcast-sender="false" | Suppresses echoing the change back to the sender |
broadcast-client="false" | Suppresses propagation to other tabs |
form-attributes="false" | Prevents the element from inheriting its form's scope |
reset | Controls reset behavior, see Forms |
splice, slice, update | Array mutation modes when the key targets an index |
Key Expressions
key supports more than a plain field name.
| Expression | Resolves To |
|---|---|
name | A top-level field |
profile.address.city | A nested field via dot notation |
tags[0] | An array member by index |
{} | The entire document, for reading or writing whole objects |
tags.$length | The length of an array field |
$data | The complete response payload |
$object / $array | The full result collection for the current type |
<span array="posts" object="64b9a32e18f21bc56789abcd" key="comments.$length"></span>
Value Pipeline
Values are transformed on the way in and out by @cocreate/element-prototype. These attributes apply to any bound element.
| Attribute | Description |
|---|---|
value-type | Coerces the value, for example json, html, text, array, number |
value-prefix | Prepends a string |
value-suffix | Appends a string |
value-replace | Replaces the first match of a pattern |
value-replaceall | Replaces every match of a pattern |
value-replacement | Replacement string used by the above |
value-lowercase | Lowercases the value |
value-uppercase | Uppercases the value |
value-encode | Encodes the value, for example base64 or URI |
value-decode | Decodes the value |
value-remove-query | Removes matching nodes before reading an HTML value |
value-dispatch | Clears the value when a query returns no results |
value-bubbles | Bubbles the synthetic input event |
value-query | Composes this element's value from other elements |
value-attribute | Writes the composed value to an attribute instead of the value |
<textarea
array="files"
object="64b9a32e18f21bc56789abcd"
key="src"
value-type="text"
value-replaceall="/```html\n|\n```/g"></textarea>
Forms
Scope Inheritance
A form propagates organization_id, host, storage, database, array, index, object, and key to descendant elements that participate in binding. Add form-attributes="false" to an element that must keep its own scope.
Generated Ids
object="ObjectId()" generates a client-side id at initialization, letting related records reference each other before they are persisted.
Saving
Saving a form collects every bound field into one payload per type and issues a single operation. Saving an individual element inside a form delegates to the form, so a field never writes a partial document by accident.
Reset
The reset attribute controls what a reset preserves.
| Value | Document Id | Value |
|---|---|---|
| omitted | cleared | cleared |
object | cleared | preserved |
value | preserved | cleared |
false | preserved | preserved |
<input key="organization_id" value="$organization_id" reset="false" hidden />
Filtering and Lists
Filter attributes from @cocreate/filter compose directly with a bound scope to drive queries, sorting, limits, and pagination.
<div
array="posts"
object
filter-query-key="status"
filter-query-value="published"
filter-sort-key="created.on"
filter-sort-direction="desc"
filter-limit="10"
render-query="[template]">
<div filter-on="scroll" filter-query="$parent"></div>
<article template>{{object.title}}</article>
</div>
A filter element may target another element with filter-query, so an input elsewhere on the page can drive a list's query. Changing a filter re-reads the scope and re-renders.
Realtime Synchronization
Every scope subscribes over the socket on two channels: one for the specific document and one for the collection it belongs to. A change therefore reaches both the element bound to that record and any list rendering the collection.
Subscriptions are reference counted. When the last element bound to a scope is removed from the DOM, the module unsubscribes automatically, so rendered lists and modals do not leak channels.
Actions
| Action | Completion Event | Behavior |
|---|---|---|
save | saved | Persists the element, or its form when it belongs to one |
delete | deleted | Deletes the bound document, honoring delete-query for multiple targets |
reset | reset | Restores the form according to each field's reset attribute |
<button actions="save, reset">Save and clear</button>
Element Methods
Binding attaches methods directly to elements, so imperative code and action chains share one implementation.
| Method | Available On | Description |
|---|---|---|
getValue() | any bound element | Reads the value through the value pipeline |
setValue(value) | any bound element | Writes the value and dispatches input and change |
read() | any bound element | Re-reads the element's scope |
save() | any bound element or form | Persists the element or the whole form |
getData() | forms | Returns the assembled payload without saving |
getFilter() / setFilter() | filtered elements | Reads or replaces the active filter, re-reading the scope |
const form = document.querySelector("#profile");
const payload = await form.getData();
await form.save();
JavaScript API
import elements from "@cocreate/elements";
elements.init();
elements.init(document.querySelector("#profile"));
| Method | Description |
|---|---|
init(element) | Initializes an element, a collection, or the whole document |
read(element, data, dataKey) | Reads a scope and distributes the result |
save(element, action) | Persists an element or form |
getData(element) | Assembles the payload for an element or form |
getDataObject(element) | Normalizes an element's attributes into a CRUD payload |
reset(action) | Runs reset logic for a form |
setDataType(element, data, action) | Applies a created record's id back into the markup |
elements, keys, forms | Internal registries of bound elements and scopes |
Behavior Notes
- An element must declare
storage,database, orarrayto be initialized. A loneobjectorkeyis not a binding, which is why fields inside a form rely on inherited scope. - Reads are debounced for 500ms and deduplicated per scope, so many elements binding one document produce a single query.
- An element reads once per scope. Changing a filter or calling
read()resets that state. - A document id must be a 24 character hexadecimal value, or the placeholders
pendingandObjectId(). - Elements rendered on the server carry a
renderedmarker and skip their first client render, preserving server output. - Save on input requires
realtime, and never applies to a_idfield. - When
crdtis enabled and a document id exists, text edits merge collaboratively rather than overwriting.
Announcements
All updates to this library are documented in our CHANGELOG and releases. You may also subscribe to email for releases and breaking changes.
Roadmap
Upcoming enhancements include:
- Expanded array mutation operators
- Optimistic write rollback on failure
- Schema-aware value coercion
- Development mode diagnostics for unbound and misconfigured scopes
- Devtools inspection of an element's scope and subscriptions
If you are interested in the future direction of this project, please take a look at our open issues and pull requests. We would love to hear your feedback.
How to Contribute
We encourage contribution to our libraries (you might even score some nifty swag), please see our CONTRIBUTING guide for details.
We want this library to be community-driven, and CoCreate led. We need your help to realize this goal. To help make sure we are building the right things in the right order, we ask that you create issues and pull requests or merely upvote or comment on existing issues or pull requests.
We appreciate your continued support, thank you!
About
CoCreate-elements is guided and supported by the CoCreate Developer Experience Team.
Please Email the Developer Experience Team here in case of any queries.
CoCreate-elements is maintained and funded by CoCreate. The names and logos for CoCreate are trademarks of CoCreate, LLC.
License
This software is dual-licensed under the GNU Affero General Public License version 3 (AGPLv3) and a commercial license.
-
Open Source Use: For open-source projects and non-commercial use, this software is available under the AGPLv3. The AGPLv3 allows you to freely use, modify, and distribute this software, provided that all modifications and derivative works are also licensed under the AGPLv3. For the full license text, see the LICENSE file.
-
Commercial Use: For-profit companies and individuals intending to use this software for commercial purposes must obtain a commercial license. The commercial license is available when you sign up for an API key on our website. This license permits proprietary use and modification of the software without the copyleft requirements of the AGPLv3. It is ideal for integrating this software into proprietary commercial products and applications.
If you have not purchased a commercial license and intend to use this software for commercial purposes, you are required to sign up for an API key on our website.
Enter shortcuts for action buttons
Set submit-query on a form or an inner section to explicitly select the action
element Enter should click from a single-line input:
<form>
<section submit-query="#next-step">
<input name="name" />
<button id="next-step" type="button" actions="disabled, validate, save, disabled">Next</button>
</section>
</form>
The nearest submit-query within the form wins. Its value uses CoCreate query
selectors, resolved from the element carrying the attribute. The resolved
target must have actions, belong to the same form, and be visible and enabled.
No action target is inferred when the attribute is absent. Empty or unresolved
queries, multiple returned targets, and disabled targets do not fall through to
native submission. Held Enter does not repeatedly activate the button.
Textareas, selects, editable content, input widgets, composition, modified
keystrokes, and events already handled by another listener keep their normal
keyboard behavior. This shortcut calls .click(); it does not submit the form
itself. Existing action/method handling remains separate, and a submit button
with actions can still preprocess an external submission through its chain.