-
Notifications
You must be signed in to change notification settings - Fork 10
agent-feedback: marko@next install, custom element semantics and ctx.body #286
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 |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| --- | ||
| type: dx | ||
| impact: med | ||
| effort: low | ||
| site: docs/introduction/installation.md › ## Manual Setup | ||
| --- | ||
|
|
||
| # Document the browser-only setup (`marko({ linked: false })`, `index.html`, `Template.mount`) under Manual Setup | ||
|
|
||
| `docs/introduction/installation.md` › ## Manual Setup walks through Vite plus `marko()` plus an express SSR server, and the browser-only alternative survives as a parenthetical on the "Add a server" step ("this can be disabled with the Marko plugin's `linked` option"). `linked` appears in one other place under `docs/`, the `docs/reference/lazy-loading.md` note that `linked: false` cannot code-split; `docs/introduction/integrations.md` covers bundlers with no browser build; and `docs/reference/template.md` › ## `Template.mount(input, node, position?)` documents the API, warning that it is "primarily intended to be used in exclusively client rendered environments", without tying it to a build setup. Following Manual Setup and dropping the server file without also passing `linked: false` fails the build with `[marko-vite:pre] You must run the "ssr" build before the "browser" build.`, an error the docs do not explain. Add a Manual Setup subsection assembling the three files a client-only app needs, a `vite.config.ts` with `marko({ linked: false })`, an `index.html` holding a mount node and a module script, and an entry module calling `Template.mount`; carry the lazy-loading note that `linked: false` cannot code-split and link the `Template.mount` section. The site already runs this mode itself in the playground (`src/util/workspace.ts`). | ||
|
|
||
| Check: `grep -rn '\blinked\b' docs` prints only `docs/introduction/installation.md:88` (the parenthetical) and `docs/reference/lazy-loading.md:180`, and `grep -rni 'client-only\|SPA' docs` prints nothing. In a scratch dir with marko 6.3.46, @marko/vite 6.1.11 and vite 8.2.2, with `vite.config.ts` = `export default defineConfig({ plugins: [marko({ linked: false })] })`, `index.html` = `<div id="app"></div><script type="module" src="/src/main.ts"></script>`, `src/app.marko` = `<let/count=input.start ?? 0><button onClick() { count++ }>Clicked ${count}</button>` and `src/main.ts` = `App.mount({ start: 3 }, document.getElementById("app")!)`, `npx vite build` prints `✓ 7 modules transformed` plus `dist/assets/index-*.js 4.76 kB` and `npx vite preview` serves a page whose button goes from `Clicked 3` to `Clicked 4` with no server file. The same project with plain `marko()` fails `npx vite build` with `You must run the "ssr" build before the "browser" build.` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| --- | ||
| type: dx | ||
| impact: med | ||
| effort: low | ||
| site: docs/reference/core-tag.md › ### `@catch` | ||
| --- | ||
|
|
||
| # Scope `@catch` to errors thrown while rendering the `<try>` content | ||
|
|
||
| `docs/reference/core-tag.md` › ### `@catch` says "When a runtime error occurs in the content of the `<try>` or its `@placeholder` attribute tag, the content is replaced", which a reader takes to include the event handlers and effects written in that content. The runtime scopes it to the render path: marko `dom/catch.feat.ts` wraps `runRender` alone, with a comment recording that an error thrown from a `<script>` or `<lifecycle>` body deliberately escapes the flush, and `dom/event.ts` › `handleDelegated` invokes handlers bare. Clicking a `<button onClick() { throw new Error("event boom") }>` inside a `<try>` leaves `@catch` unrendered, leaves the sibling content in place and surfaces one uncaught page error; a `<script>` that throws behaves the same on its first mount and on an update, while a throw during render, initial or on update, and a rejected `<await>` are both caught. Restate the section as the positive rule, that `@catch` covers errors thrown while rendering the content including a rejected `<await>`, and point handler and effect errors at the ordinary window error path. | ||
|
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 Preserve the The existing contract covers errors from both 🤖 Prompt for AI Agents |
||
|
|
||
| Check: mount `<try><widget mode=mode/><@catch|err|><div class="caught">caught: ${err.message}</div></@catch></try>`, where widget is `<const/status=health(input.mode)><div class="status">Status: ${status}</div><button onClick() { throw new Error("event boom") }>Poke</button>` and `health` throws for `mode === "render"`. Clicking Poke gives `{ caughtCount: 0, status: "Status: healthy", pageErrors: ["event boom"] }`; flipping `mode` to `"render"` gives `{ caughtCount: 1, caughtText: "caught: render boom", statusCount: 0 }`; a `<try>`-wrapped child whose `<script>` throws gives `{ caughtCount: 0, errs: ["effect boom"] }` with its sibling content still rendered; `<try><await|v|=Promise.reject(new Error("await boom"))>` renders `@catch` with an empty `pageErrors`. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| --- | ||
| type: bug | ||
| impact: med | ||
| effort: low | ||
| site: docs/explanation/controllable-components.md › ### Controllable `<let>` | ||
| --- | ||
|
|
||
| # Give the controllable `<counter>` example an optional `count` and a `?? 0` default | ||
|
|
||
| The counter.marko block under `docs/explanation/controllable-components.md` › ### Controllable `<let>` declares `count: number` as required and binds `<let/count=input.count valueChange=input.countChange>`, while the parent block on the same page renders a bare `<counter/>` labelled "This one holds its own state". `mtc` rejects that usage with TS2345 (`Property 'count' is missing in type '{}' but required in type 'Input'`), and silencing the error does not rescue it: `input.count` is undefined, so the server renders `Count: ` where the page's own bullet promises the behavior of its first example, and the first click makes it NaN. The shape that keeps the uncontrolled, seeded and controlled usages all valid is `count?: number` with `<let/count=input.count ?? 0 valueChange=input.countChange>`, which type-checks for `<counter/>`, `<counter count=5/>`, `<counter count=parentCount countChange(count) {...}/>` and `<counter count:=parentCount/>`. Note in the section that the explicit handler form is required once a default is involved, since `<let/count:=input.count ?? 0>` is a compile error; `docs/reference/core-tag.md` › ### Controllable Let is self-consistent and needs no change. | ||
|
|
||
| Check: copy the two blocks under ### Controllable `<let>` verbatim into a project with `@marko/type-check` 3.2.0 as `src/tags/counter.marko` and `src/parent.marko` and run `npx mtc`: it reports `error TS2345` on `<counter/>` in `src/parent.marko` with `Property 'count' is missing in type '{}' but required in type 'Input'`, unchanged with `strict: false`. In the marko checkout, `pnpm run compile -- -o html -d <parent.marko>` and rendering the result gives `<button>Count: <!>0<!--...--></button><button>Count: <!><!--...--></button>`, the second button carrying no number. With `count?: number` and `?? 0`, `npx mtc` reports nothing for the four usages above, while `<let/count:=input.count ?? 0>` fails `pnpm run compile -- -o dom -d` with "Attributes may only be bound to identifiers or member expressions". |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| --- | ||
| type: dx | ||
| impact: med | ||
| effort: low | ||
| site: docs/reference/typescript.md › ### Registering a new native tag (e.g. for custom elements) | ||
| --- | ||
|
|
||
| # State that attributes on a registered custom element are written with `setAttribute`, never as properties | ||
|
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. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Add the required TLDR callout. This Markdown file has no Based on learnings: Markdown files must use a 🤖 Prompt for AI AgentsSource: Learnings |
||
|
|
||
| Every attribute on a `marko.json`-registered custom element goes through marko `packages/runtime-tags/src/dom/dom.ts` › `_attr`, which is `setAttribute(element, name, normalizeAttrValue(value))` with no `name in element` or `customElements.get` branch, and the translator special-cases controllable properties for input, select, textarea, details and dialog alone, so a same-named property setter on the element class is never called. The example under `docs/reference/typescript.md` › ### Registering a new native tag (e.g. for custom elements) reads the other way: it declares `class RangeSliderElement extends HTMLElement { value = 0 }` beside `<range-slider/sliderEl value=threshold step=5 .../>`, so the class field holds its initial value while `getAttribute("value")` tracks the binding, and the class declares no `observedAttributes`, so as printed nothing in it picks the attribute up. State the positive rule on that section and on `docs/reference/native-tag.md`, and show the element-reference idiom for writing a property, `<x-datepicker/picker value=newDate/>` with `picker().value = newDate` from a `<script>` body. `docs/guide/library-integration.md` › ### Consuming in Marko and ### Using Marko in a Web Component are empty headings and are the natural second home for it. | ||
|
|
||
| Check: `sed -n '/export function _attr(/,/^}/p' packages/runtime-tags/src/dom/dom.ts` in the marko checkout shows the `setAttribute` call as the whole body. In a `linked: false` Vite page with `marko.json` = `{"<x-datepicker>":{"html":true}}` and an `x-datepicker` class defining `static get observedAttributes(){return["value"]}` plus a counting `set value(v)` and `attributeChangedCallback`, rendering `<let/newDate="2025-06-15"/><x-datepicker value=newDate/>` and then reassigning `newDate` reports `{ propertySets: 0, attributeSets: 2, setterCalls: [] }` with `outerHTML` `<x-datepicker value="2026-01-01"></x-datepicker>`. Rendering the typescript.md example with its class registered leaves `el.value` at its class-field value while `el.getAttribute("value")` goes `20` then `42`. | ||
|
Comment on lines
+10
to
+12
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 🔎 Supported by static analysis🌐 Web query:
💡 Result: Yes, Element.setAttribute invokes the attributeChangedCallback lifecycle callback for custom elements, provided that the attribute name is included in the element's static observedAttributes array [1][2][3]. When setAttribute is called, the browser checks if the attribute is in the observedAttributes list; if it is, the attributeChangedCallback is triggered [1][2]. This occurs whether the attribute is being added for the first time or its value is being updated (even if the value is technically the same) [4][1]. It is common practice for the attributeChangedCallback to assign the value to a corresponding property [2][3][5]. Developers typically implement this by using a class property setter that reflects the value back to the attribute, or by having the attributeChangedCallback update an internal property or state [2][3][5]. To prevent infinite loops when syncing properties and attributes, developers should include conditional checks to ensure the attribute or property is only updated if the new value differs from the current one [5][6]. Example of proper property-attribute synchronization: class MyElement extends HTMLElement { static get observedAttributes { return ['value']; } attributeChangedCallback(name, oldValue, newValue) { if (oldValue!== newValue) { this._value = newValue; // Sync property to attribute change } } get value { return this._value; } set value(val) { if (this.value!== val) { this.setAttribute('value', val); // Sync attribute to property change } } } Citations:
🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- available repository knowledge ---'
find /tmp/coderabbit-repo-knowledge/marko-js-website-efb9681e -maxdepth 2 -type f -name '*.md' -print
printf '%s\n' '--- convention headers ---'
for f in /tmp/coderabbit-repo-knowledge/marko-js-website-efb9681e/*/*.md; do
printf '\n### %s\n' "$f"
head -5 "$f"
done
printf '%s\n' '--- reviewed file ---'
cat -n agent-feedback/items/2026-08-28-custom-element-attributes-setattribute.mdRepository: marko-js/website Length of output: 8307 🏁 Script executed: #!/bin/bash
set -eu
for f in \
/tmp/coderabbit-repo-knowledge/marko-js-website-efb9681e/conventions/docs-reference.md \
/tmp/coderabbit-repo-knowledge/marko-js-website-efb9681e/conventions/agent-feedback-items.md \
/tmp/coderabbit-repo-knowledge/marko-js-website-efb9681e/learnings/repo-wide.md \
/tmp/coderabbit-repo-knowledge/marko-js-website-efb9681e/learnings/docs-marko-run.md
do
printf '\n--- %s ---\n' "$f"
cat "$f"
doneRepository: marko-js/website Length of output: 13711 Limit the setter claim to Marko’s direct behavior.
🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| --- | ||
| type: dx | ||
| impact: low | ||
| effort: low | ||
| site: docs/reference/typescript.md › ### Registering a new native tag (e.g. for custom elements) | ||
| --- | ||
|
|
||
| # Show how to declare a custom element's `CustomEvent` handler in the native tag registration | ||
|
|
||
| `AttrEventHandler` (marko `packages/runtime-tags/tags-html.d.ts`) brands a handler's event as `Event` intersected with a `currentTarget` string literal carrying the delegation guidance, deliberate since #3817. Any `currentTarget` brand makes that type non-comparable with `CustomEvent<T>`, so a handler inherited from `Marko.HTMLAttributes` cannot reach a custom element's `detail` the ordinary way: `(e as CustomEvent<string>).detail` is TS2352, annotating the parameter is TS2322, and re-declaring `onChange` in an interface that extends `Marko.HTMLAttributes<T>` is TS2430; only a widening double cast compiles. The supported route is to declare the event on the attributes interface, which for a custom name needs nothing extra (`interface XAttrs extends Marko.HTMLAttributes<T> { onDatepickerChange?: (e: CustomEvent<string>, target: T) => void }`) and for a name colliding with a standard DOM event needs `extends Omit<Marko.HTMLAttributes<T>, "onChange">`. Add both spellings, and the collision rule, to the registration section, which today shows `onChange(evt, target) { target.value }` alone. | ||
|
|
||
| Check: in a project with `@marko/type-check` 3.2.0 and `marko.json` `{ "<x-datepicker>": { "html": true } }`, `npx mtc -p tsconfig.json -d condensed` over `<div onChange(e) { const d: string = (e as CustomEvent<string>).detail }/>` reports `error TS2352 Conversion of type 'Event & { currentTarget: "Marko delegates events, ..."; }' to type 'CustomEvent<string>' may be a mistake`, and over `<div onChange(e: CustomEvent<string>) { console.log(e.detail) }/>` reports `error TS2322 Type '(e: CustomEvent<string>) => void' is not assignable to type 'AttrEventHandler<Event, HTMLDivElement>'`, while `e as Event as CustomEvent<string>` and `e as unknown as CustomEvent<string>` report nothing. Registering `interface DatepickerAttributes extends Omit<Marko.HTMLAttributes<DatepickerElement>, "onChange"> { value?: string; onChange?: (e: CustomEvent<string>, target: DatepickerElement) => void }` as `Marko.NativeTags["x-datepicker"]` leaves `<x-datepicker value=picked onChange(e, target) { const d: string = e.detail } onClick(e, target) { console.log(e.clientX, target.value) }/>` clean, while `e.clientX` inside `onChange` still reports TS2339. |
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.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: marko-js/website
Length of output: 11685
🏁 Script executed:
Repository: marko-js/website
Length of output: 8364
Leave the documentation unchanged. The required
agent-feedback/search already returnsagent-feedback/items/2026-07-30-script-tag-run-order.md, which records this defect. Keep the defect inagent-feedback/instead of adding the proposed documentation changes.🤖 Prompt for AI Agents
Source: Path instructions