-
Notifications
You must be signed in to change notification settings - Fork 51
feat(model): add two-way data binding via ::prop in comark core #437
#438
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -118,6 +118,8 @@ Passing a document to `<Markdown>` skips parsing at runtime, but the **parser is | |
| | `summary` | `boolean` | `false` | Only render content before `<!-- more -->` | | ||
| | [`caret`](#streaming-caret) | `boolean \| { class: string }` | `false` | Append caret to last text node | | ||
| | [`data`](#code-markdown-props-code-data) | `Record<string, unknown>` | `{}` | Runtime values referenced from markdown via `:prop="data.path"` | | ||
| | [`model`](#two-way-model-binding) | `ComarkModel` | `undefined` | Two-way binding model for `::prop="path"` attributes. Omit to use an internal uncontrolled store. | | ||
| | `onModelChange` | `(path, value, snapshot) => void` | `undefined` | Observer called after every accepted write. Use for controlled-mode sync. | | ||
|
|
||
| #### `options` | ||
|
|
||
|
|
@@ -452,6 +454,8 @@ const document = await res.json() | |
| | [`streaming`](#streaming) | `boolean` | `false` | Enable streaming mode | | ||
| | [`caret`](#streaming-caret) | `boolean \| { class: string }` | `false` | Append a blinking caret to the last text node | | ||
| | [`data`](#code-markdown-props-code-data) | `Record<string, unknown>` | `{}` | Runtime values referenced from markdown via `:prop="data.path"` | | ||
| | [`model`](#two-way-model-binding) | `ComarkModel` | `undefined` | Two-way binding model for `::prop="path"` attributes. Omit to use an internal uncontrolled store. | | ||
| | `onModelChange` | `(path, value, snapshot) => void` | `undefined` | Observer called after every accepted write. Use for controlled-mode sync. | | ||
|
|
||
| ### `defineMarkdownDocumentComponent` | ||
|
|
||
|
|
@@ -815,4 +819,90 @@ defineProps<{ | |
| </script> | ||
| ``` | ||
|
|
||
| ## Two-way model binding | ||
|
|
||
| Use `::prop="data.path"` in your markdown to wire native form elements or pass live values to custom components. No event handlers needed in the markdown itself. | ||
|
|
||
| ### Uncontrolled (default) | ||
|
|
||
| When no `model` prop is provided, `<Markdown>` and `<MarkdownDocument>` create an internal `createModelStore()` seeded from `data`. Writes stay local to the component. | ||
|
|
||
| ```vue [App.vue] | ||
| <script setup lang="ts"> | ||
| import { Markdown } from '@comark/vue' | ||
| import binding, { Binding, If } from '@comark/vue/plugins/binding' | ||
|
|
||
| const content = ` | ||
| Name: {{ data.name }} | ||
|
|
||
| :input{::value="data.name" type="text"} | ||
| ` | ||
| </script> | ||
|
|
||
| <template> | ||
| <Suspense> | ||
| <Markdown | ||
| :value="content" | ||
| :data="{ name: 'Ada' }" | ||
| :plugins="[binding()]" | ||
| :components="{ Binding, If }" | ||
| /> | ||
| </Suspense> | ||
| </template> | ||
| ``` | ||
|
|
||
| ### Controlled | ||
|
|
||
| Pass your own `ComarkModel` to share state between components or observe changes externally. | ||
|
|
||
| ```vue [App.vue] | ||
| <script setup lang="ts"> | ||
| import { ref } from 'vue' | ||
| import { Markdown } from '@comark/vue' | ||
| import binding, { Binding, If } from '@comark/vue/plugins/binding' | ||
| import { createModelStore } from 'comark/model' | ||
|
|
||
| const model = createModelStore({ data: { data: { name: 'Ada' } } }) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Remove the extra
🤖 Prompt for AI Agents |
||
| const name = ref(model.get('data.name')) | ||
| model.subscribe('data.name', (v) => { name.value = v as string }) | ||
| </script> | ||
|
|
||
| <template> | ||
| <p>Current name: {{ name }}</p> | ||
| <Suspense> | ||
| <Markdown | ||
| value=":input{::value=\"data.name\" type=\"text\"}" | ||
| :model="model" | ||
| :plugins="[binding()]" | ||
| :components="{ Binding, If }" | ||
| /> | ||
| </Suspense> | ||
| </template> | ||
| ``` | ||
|
|
||
| ### Native form elements | ||
|
|
||
| `::value`, `::checked`, and `::files` on native `<input>`, `<select>`, and `<textarea>` are wired automatically. Vue events (`onInput`, `onChange`) and controlled-element props are set for you. | ||
|
|
||
| | Markdown | Element | Wired prop | Event | | ||
| |----------|---------|------------|-------| | ||
| | `:input{::value="…" type="text"}` | `<input>` | `value` | `onInput` | | ||
| | `:input{::checked="…" type="checkbox"}` | `<input>` | `checked` | `onChange` | | ||
| | `:input{::value="…" type="number"}` | `<input>` | `value` (as number) | `onInput` | | ||
| | `:select{::value="…"}` | `<select>` | `value` | `onChange` | | ||
| | `:textarea{::value="…"}` | `<textarea>` | `value` | `onInput` | | ||
|
|
||
| For custom components, `onUpdate:prop` (the native Vue v-model handler shape) is passed directly, so `v-model`-compatible components work out of the box. | ||
|
|
||
| ### `onModelChange` | ||
|
|
||
| Observe every write without subscribing to the model manually: | ||
|
|
||
| ```vue | ||
| <Markdown | ||
| :value="content" | ||
| :on-model-change="(path, value, snapshot) => console.log(path, value)" | ||
| /> | ||
| ``` | ||
|
|
||
| --- | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Correct the no-JavaScript behavior statement.
Without
initComarkRuntime(), the input can accept local edits, but{{ data.name }}does not update and no model write occurs. State that SSR provides the initial read value only. State that two-way synchronization requires the runtime.The runtime implementation updates bind markers only after
inputorchangeevents.🤖 Prompt for AI Agents