yrby-forms: collaborative form fields (gem + npm elements) - #73
Open
jpcamara wants to merge 1 commit into
Open
Conversation
A third gem (yrby-forms) and a second npm package (packages/forms) that
make plain Rails form fields collaborate over one shared document per
record.
The gem:
- has_collaborative_fields declares which attributes collaborate. Tier
detection per attribute: Rails enums are LWW, string/text columns are
text tier (Y.Text), everything else is LWW; text:/lww: kwargs force a
tier and an unknown attribute raises at declaration. encrypted: true
stores state through Y::EncryptedDocument.
- One Y::Document named "fields" per record: LWW entries in a "fields"
map share, one "fields/<name>" Y.Text share per text-tier field.
- refresh_collaborative_fields materializes the document into the
declared columns under the record lock (save! validate: false), casting
LWW values through the attribute types. Undeclared map keys are dropped
before assign_attributes: the map is client-written, so this is the
line between collaboration and mass assignment.
- Form helpers: form.collaborative_fields renders the
<collaborative-form> container (channel, purpose-scoped signed
GlobalID, doc key, identity from Y::Forms.identity);
form.collaborative_field wraps the stock input for the attribute (or a
block-supplied one) in <collaborative-field name tier>.
- yrby_forms:install generates a FormFieldsChannel (locate_signed scoped
to Y::Forms.sgid_purpose, deny-by-default authorized?, storage through
the record's document, refresh after every append) and invokes
yrby:tables for the migration.
The npm package (packages/forms, same tsc dual-build + node --test
tooling as packages/client):
- <collaborative-form> builds the consumer (createConsumer, or
setConsumer / a consumer property for AnyCable), a Y.Doc, and
yrby-client's ActionCableProvider from its attributes, and reflects the
provider status into its status attribute.
- <collaborative-field> binds the control inside it by tier: LWW is map
entry <-> control value on input/change, remote-echo-safe via
transaction origins; text is a minimal diff-splice binding between the
control and the Y.Text, keeping the local caret across remote splices.
- Presence: awareness carries { name, color, field } set on focus/blur;
a field a remote peer is in shows their color as an outline and their
name as a chip (one injected stylesheet, override or pre-insert by id).
Demo + e2e: the actioncable-demo gains a /tickets/:id page running the
whole stack (Ticket model, FormFieldsChannel over Y::Document storage,
the helpers, the elements) and frontend/form_fields_e2e.mjs drives two
real browsers through it in CI: LWW last-write-wins convergence, text
merge of concurrent typing, presence on the peer, and materialized
columns matching the converged state. New CI job runs the package tests
on Node 20/22.
Both existing gemspecs exclude the forms files (packaging_test now pins
the three-way separation), and the demo Dockerfile builds the new
package's dist like yrby-client's.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a third gem,
yrby-forms, and a second npm package,packages/forms(npmyrby-forms): plain Rails form fields that collaborate over one shared document per record, with the values materialized back into the columns.The gem
has_collaborative_fields :status, :priority, :summary, :descriptiondeclares which attributes collaborate. Tiers resolve per attribute: Rails enums are LWW,:string/:textcolumns are text tier (Y.Text), everything else is LWW.text:/lww:kwargs force a tier and win over detection; an unknown attribute raises at declaration;encrypted: truestores CRDT state throughY::EncryptedDocument.Y::Documentnamed"fields"per record for the whole set (theY::Document.forassociation pattern): LWW entries live in a"fields"map share, each text-tier field in a"fields/<name>"Y.Text share.refresh_collaborative_fieldsreloads the document under the record lock, reads it (read_map/read_text), assigns the declared fields, and saves withvalidate: false— false when the document has no state. LWW values cast through the attribute types. Undeclared map keys never reachassign_attributes: the map is client-written, so this filter is the line between collaboration and mass assignment.form.collaborative_fieldsrenders the<collaborative-form>container (channel name, signed GlobalID scoped toY::Forms.sgid_purpose, doc key, presence identity fromY::Forms.identity);form.collaborative_field :statuswraps the stock input for the attribute — or a block-supplied one — in<collaborative-field name tier>.yrby_forms:installgenerates a FormFieldsChannel (signed-GlobalID locate, deny-by-defaultauthorized?, storage through the record's document, refresh after every append) and invokesyrby:tables.The npm package
Two custom elements, no framework, same tsc dual-build +
node --testtooling aspackages/client:<collaborative-form>builds the consumer (createConsumer, orsetConsumer/ aconsumerproperty for AnyCable), aY.Doc, and yrby-client'sActionCableProviderfrom its attributes, and reflects the provider status into itsstatusattribute.<collaborative-field>binds the one control inside it by tier. LWW: map entry ↔ control value on input/change, remote-echo-safe via transaction origins, checkboxes as booleans. Text: a minimal diff-splice binding (common prefix/suffix → one delete + one insert) between the control and the Y.Text, keeping the local caret across remote splices.{ name, color, field }set on focus/blur; a field a remote peer is in shows their color as an outline and their name as a chip. One injected stylesheet, suppressible by id.Tests and e2e
FormFieldsfixture in the generator), undeclared-key rejection, the encrypted variant, helper output (sgid purpose scoping included), and the generator.packaging_testnow pins the three-way gemspec separation.node --testunder jsdom drives the real elements — two wired forms converging an LWW select and a checkbox, concurrent text merge, caret preservation across a remote splice, presence chip/outline rendering, status reflection./tickets/:id(Ticket model, FormFieldsChannel overY::Documentstorage, the helpers, the packaged elements) andfrontend/form_fields_e2e.mjsdrives two real browsers: the LWW field converges last-write-wins, the text field merges concurrent typing, presence appears on the peer, and the materialized columns match the converged state (a == columns == b). Runs in the demo job's Puma section; a newformsCI job runs the package tests on Node 20/22.Docs
README-forms.mddocuments the whole system, including the identity posture (names are cosmetic, access is enforced by the sgid +authorized?) and the documented limitation: no server→document write-back until yrby#33 — the document is client-authored, the columns are the materialized view. Root README gains a one-paragraph pointer;CHANGELOG-forms.mdstarts the gem's changelog.