diff --git a/.cursorrules b/.cursorrules index 476701c4..438d5981 100644 --- a/.cursorrules +++ b/.cursorrules @@ -9,6 +9,8 @@ - Use "we" in tutorials when building something together - Be matter-of-fact and precise in reference docs - Use encouraging but professional language in tutorials ("Aha!", "Great!") +- Use measured language for removed/deprecated features: "have been removed", "no longer necessary", "no longer supported". Never "gone" or "gone entirely". +- Reference other frameworks only when linking to a genuinely useful resource (e.g., React's "You Might Not Need an Effect"). Do not list frameworks for favorable comparison (e.g., avoid "This aligns Marko with React, Svelte, and Vue"). ## Headings and Structure @@ -39,21 +41,29 @@ ### Explanation Documents - Include TLDR section with 2-4 very brief "at a glance" bullets. Fragments are allowed and usually should not end with periods. Do not include a blank line after the TLDR callout. +- After the TLDR, include a short introductory paragraph that frames the document's purpose, audience, and context before diving into sections. Write it as natural prose that orients the reader, never as a table of contents. Specifically, never begin with "This document explains..." or "This page covers..." or any enumeration of the page's sections. Instead, lead with the core idea or the reader's starting point. + - ❌ "This document explains what X means, why Y is preferred, and how Z works." + - ✅ "Developers familiar with the Class API often relied on `this.emit`... Marko 6 introduces a first-class pattern that removes much of this ceremony." + - ✅ "Marko pushes work from runtime to compile time. The compiler analyzes templates and generates environment-specific output that avoids common runtime overhead." - Focus on "why" and conceptual understanding - Use **bold** and _italics_ sparingly for truly key concepts only - Provide practical guidance and best practices - Include real-world examples and use cases - Maintain a technical but slightly conversational tone +- Explicitly label anti-patterns and explain why they should be avoided +- Present the most common or important pattern first when a section covers multiple alternatives +- Prefer flat heading hierarchy (##) when sections are substantial enough to stand alone. Do not nest topics under umbrella headings just for grouping. ## Code Examples and Technical Content - Every concept should have a concise, focused code example - Examples must be minimal and strictly relevant -- Add filenames to code examples with a `/* filename.extension */` on the first line +- Add filenames to code examples with `/* filename.extension */` on the first line ONLY when two or more code blocks appear together and need disambiguating (e.g., parent.marko / child.marko). A single standalone code block must NEVER have a filename comment. Do not append metadata to filename comments (use `/* counter.marko */`, not `/* counter.marko - Class API */`). - Examples must be unique across all documentation - avoid reusing common tropes - In Marko files, use JS-style comments (`// comment` or `/* comment */`) instead of HTML comments (``) - Avoid the `**Bold Title**: description` list pattern which sounds LLM-generated - In surrounding prose, avoid calling examples "minimal" or "simple". Prefer neutral lead-ins such as "Consider this Marko template" or "Example". +- Prefer practical, real-world examples that demonstrate actual patterns (e.g., event spread, focus management, form handling) over abstract or hello-world level demos ## Callouts and Formatting @@ -74,6 +84,17 @@ - Reference specific sections using anchors - Link to external resources (MDN, etc.) for web standards - Maintain consistency in how concepts are referenced +- When discussing removed or changed APIs, link to the old documentation (e.g., v5.markojs.com) so readers can understand what is being replaced +- When discussing removed APIs, link to their modern equivalents elsewhere in the docs (e.g., anchor links within the same page or to reference docs) + +## Migration and Comparison Content + +- Frame API changes constructively: use "Updated APIs" or "Modern equivalents" instead of "Removed APIs" or "What's gone" +- When comparing old and new approaches, show the old approach first so readers can connect with what they know, then the new replacement +- Mention the modern equivalent alongside every removal. Never just list what was removed without showing the path forward. +- Use callouts (NOTE, CAUTION, WARNING) to flag anti-patterns, important conventions, and gotchas. Do not leave these implicit in prose. +- Vary section structure naturally. Do not use a formulaic repeating sub-heading pattern like "### Old Way" / "### New Way" in every section. Integrate the before/after comparison organically within each section's prose and code examples. +- Embed anti-pattern warnings inline (using callouts or prose) near the relevant content. Do not collect anti-patterns into a separate "Anti-Patterns" section at the end. ## Content Philosophy diff --git a/.marko-run/routes.d.ts b/.marko-run/routes.d.ts index 66efb4a1..96ecc5f9 100644 --- a/.marko-run/routes.d.ts +++ b/.marko-run/routes.d.ts @@ -13,6 +13,7 @@ declare module "@marko/run" { "/": { verb: "get"; }; "/brand": { verb: "get"; }; "/docs": { verb: "get"; }; + "/docs/explanation/class-vs-tags-api": { verb: "get"; meta: typeof import("../src/routes/docs/_compiled-docs/explanation/class-vs-tags-api+meta.json"); }; "/docs/explanation/controllable-components": { verb: "get"; meta: typeof import("../src/routes/docs/_compiled-docs/explanation/controllable-components+meta.json"); }; "/docs/explanation/fine-grained-bundling": { verb: "get"; meta: typeof import("../src/routes/docs/_compiled-docs/explanation/fine-grained-bundling+meta.json"); }; "/docs/explanation/immutable-state": { verb: "get"; meta: typeof import("../src/routes/docs/_compiled-docs/explanation/immutable-state+meta.json"); }; @@ -94,7 +95,7 @@ declare module "../src/routes/docs/_llms/reference-full%2emd+handler" { declare module "../src/routes/docs/+middleware" { namespace MarkoRun { export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; - export type Route = Run.Routes["/docs" | "/docs/explanation/controllable-components" | "/docs/explanation/fine-grained-bundling" | "/docs/explanation/immutable-state" | "/docs/explanation/let-vs-const" | "/docs/explanation/nested-reactivity" | "/docs/explanation/optimizing-performance" | "/docs/explanation/separation-of-concerns" | "/docs/explanation/serializable-state" | "/docs/explanation/streaming" | "/docs/explanation/targeted-compilation" | "/docs/explanation/why-is-marko-fast" | "/docs/guide/duplicate-form-submissions" | "/docs/guide/library-integration" | "/docs/guide/low-level-apis" | "/docs/guide/marko-5-interop" | "/docs/guide/publishing-components" | "/docs/guide/styling" | "/docs/introduction/getting-started" | "/docs/introduction/installation" | "/docs/introduction/integrations" | "/docs/introduction/welcome-to-marko" | "/docs/introduction/why-marko" | "/docs/marko-run/file-based-routing" | "/docs/marko-run/getting-started" | "/docs/marko-run/typescript" | "/docs/reference/concise-syntax" | "/docs/reference/core-tag" | "/docs/reference/custom-tag" | "/docs/reference/language" | "/docs/reference/native-tag" | "/docs/reference/reactivity" | "/docs/reference/supported-environments" | "/docs/reference/template" | "/docs/reference/typescript" | "/docs/tutorial/components-and-reactivity" | "/docs/tutorial/fundamentals" | "/docs/reference-full.md"]; + export type Route = Run.Routes["/docs" | "/docs/explanation/class-vs-tags-api" | "/docs/explanation/controllable-components" | "/docs/explanation/fine-grained-bundling" | "/docs/explanation/immutable-state" | "/docs/explanation/let-vs-const" | "/docs/explanation/nested-reactivity" | "/docs/explanation/optimizing-performance" | "/docs/explanation/separation-of-concerns" | "/docs/explanation/serializable-state" | "/docs/explanation/streaming" | "/docs/explanation/targeted-compilation" | "/docs/explanation/why-is-marko-fast" | "/docs/guide/duplicate-form-submissions" | "/docs/guide/library-integration" | "/docs/guide/low-level-apis" | "/docs/guide/marko-5-interop" | "/docs/guide/publishing-components" | "/docs/guide/styling" | "/docs/introduction/getting-started" | "/docs/introduction/installation" | "/docs/introduction/integrations" | "/docs/introduction/welcome-to-marko" | "/docs/introduction/why-marko" | "/docs/marko-run/file-based-routing" | "/docs/marko-run/getting-started" | "/docs/marko-run/typescript" | "/docs/reference/concise-syntax" | "/docs/reference/core-tag" | "/docs/reference/custom-tag" | "/docs/reference/language" | "/docs/reference/native-tag" | "/docs/reference/reactivity" | "/docs/reference/supported-environments" | "/docs/reference/template" | "/docs/reference/typescript" | "/docs/tutorial/components-and-reactivity" | "/docs/tutorial/fundamentals" | "/docs/reference-full.md"]; export type Context = Run.MultiRouteContext; export type Handler = Run.HandlerLike; export type GET = Run.HandlerLike; @@ -145,6 +146,24 @@ declare module "../src/routes/brand/+page.marko" { } } +declare module "../src/routes/docs/_compiled-docs/explanation/class-vs-tags-api+page.marko" { + namespace MarkoRun { + export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; + export type Route = Run.Routes["/docs/explanation/class-vs-tags-api"]; + export type Context = Run.MultiRouteContext & Marko.Global; + export type Handler = Run.HandlerLike; + export type GET = Run.HandlerLike; + export type HEAD = Run.HandlerLike; + export type POST = Run.HandlerLike; + export type PUT = Run.HandlerLike; + export type DELETE = Run.HandlerLike; + export type PATCH = Run.HandlerLike; + export type OPTIONS = Run.HandlerLike; + /** @deprecated use `((context, next) => { ... }) satisfies MarkoRun.Handler` instead */ + export const route: Run.HandlerTypeFn; + } +} + declare module "../src/routes/docs/_compiled-docs/explanation/controllable-components+page.marko" { namespace MarkoRun { export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; @@ -815,7 +834,7 @@ declare module "../src/routes/+layout.marko" { export interface Input extends Run.LayoutInput {} namespace MarkoRun { export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; - export type Route = Run.Routes["/" | "/brand" | "/docs/explanation/controllable-components" | "/docs/explanation/fine-grained-bundling" | "/docs/explanation/immutable-state" | "/docs/explanation/let-vs-const" | "/docs/explanation/nested-reactivity" | "/docs/explanation/optimizing-performance" | "/docs/explanation/separation-of-concerns" | "/docs/explanation/serializable-state" | "/docs/explanation/streaming" | "/docs/explanation/targeted-compilation" | "/docs/explanation/why-is-marko-fast" | "/docs/guide/duplicate-form-submissions" | "/docs/guide/library-integration" | "/docs/guide/low-level-apis" | "/docs/guide/marko-5-interop" | "/docs/guide/publishing-components" | "/docs/guide/styling" | "/docs/introduction/getting-started" | "/docs/introduction/installation" | "/docs/introduction/integrations" | "/docs/introduction/welcome-to-marko" | "/docs/introduction/why-marko" | "/docs/marko-run/file-based-routing" | "/docs/marko-run/getting-started" | "/docs/marko-run/typescript" | "/docs/reference/concise-syntax" | "/docs/reference/core-tag" | "/docs/reference/custom-tag" | "/docs/reference/language" | "/docs/reference/native-tag" | "/docs/reference/reactivity" | "/docs/reference/supported-environments" | "/docs/reference/template" | "/docs/reference/typescript" | "/docs/tutorial/components-and-reactivity" | "/docs/tutorial/fundamentals" | "/playground"]; + export type Route = Run.Routes["/" | "/brand" | "/docs/explanation/class-vs-tags-api" | "/docs/explanation/controllable-components" | "/docs/explanation/fine-grained-bundling" | "/docs/explanation/immutable-state" | "/docs/explanation/let-vs-const" | "/docs/explanation/nested-reactivity" | "/docs/explanation/optimizing-performance" | "/docs/explanation/separation-of-concerns" | "/docs/explanation/serializable-state" | "/docs/explanation/streaming" | "/docs/explanation/targeted-compilation" | "/docs/explanation/why-is-marko-fast" | "/docs/guide/duplicate-form-submissions" | "/docs/guide/library-integration" | "/docs/guide/low-level-apis" | "/docs/guide/marko-5-interop" | "/docs/guide/publishing-components" | "/docs/guide/styling" | "/docs/introduction/getting-started" | "/docs/introduction/installation" | "/docs/introduction/integrations" | "/docs/introduction/welcome-to-marko" | "/docs/introduction/why-marko" | "/docs/marko-run/file-based-routing" | "/docs/marko-run/getting-started" | "/docs/marko-run/typescript" | "/docs/reference/concise-syntax" | "/docs/reference/core-tag" | "/docs/reference/custom-tag" | "/docs/reference/language" | "/docs/reference/native-tag" | "/docs/reference/reactivity" | "/docs/reference/supported-environments" | "/docs/reference/template" | "/docs/reference/typescript" | "/docs/tutorial/components-and-reactivity" | "/docs/tutorial/fundamentals" | "/playground"]; export type Context = Run.MultiRouteContext & Marko.Global; export type Handler = Run.HandlerLike; export type GET = Run.HandlerLike; @@ -834,7 +853,7 @@ declare module "../src/routes/docs/+layout.marko" { export interface Input extends Run.LayoutInput {} namespace MarkoRun { export { NotHandled, NotMatched, GetPaths, PostPaths, GetablePath, GetableHref, PostablePath, PostableHref, Platform }; - export type Route = Run.Routes["/docs/explanation/controllable-components" | "/docs/explanation/fine-grained-bundling" | "/docs/explanation/immutable-state" | "/docs/explanation/let-vs-const" | "/docs/explanation/nested-reactivity" | "/docs/explanation/optimizing-performance" | "/docs/explanation/separation-of-concerns" | "/docs/explanation/serializable-state" | "/docs/explanation/streaming" | "/docs/explanation/targeted-compilation" | "/docs/explanation/why-is-marko-fast" | "/docs/guide/duplicate-form-submissions" | "/docs/guide/library-integration" | "/docs/guide/low-level-apis" | "/docs/guide/marko-5-interop" | "/docs/guide/publishing-components" | "/docs/guide/styling" | "/docs/introduction/getting-started" | "/docs/introduction/installation" | "/docs/introduction/integrations" | "/docs/introduction/welcome-to-marko" | "/docs/introduction/why-marko" | "/docs/marko-run/file-based-routing" | "/docs/marko-run/getting-started" | "/docs/marko-run/typescript" | "/docs/reference/concise-syntax" | "/docs/reference/core-tag" | "/docs/reference/custom-tag" | "/docs/reference/language" | "/docs/reference/native-tag" | "/docs/reference/reactivity" | "/docs/reference/supported-environments" | "/docs/reference/template" | "/docs/reference/typescript" | "/docs/tutorial/components-and-reactivity" | "/docs/tutorial/fundamentals"]; + export type Route = Run.Routes["/docs/explanation/class-vs-tags-api" | "/docs/explanation/controllable-components" | "/docs/explanation/fine-grained-bundling" | "/docs/explanation/immutable-state" | "/docs/explanation/let-vs-const" | "/docs/explanation/nested-reactivity" | "/docs/explanation/optimizing-performance" | "/docs/explanation/separation-of-concerns" | "/docs/explanation/serializable-state" | "/docs/explanation/streaming" | "/docs/explanation/targeted-compilation" | "/docs/explanation/why-is-marko-fast" | "/docs/guide/duplicate-form-submissions" | "/docs/guide/library-integration" | "/docs/guide/low-level-apis" | "/docs/guide/marko-5-interop" | "/docs/guide/publishing-components" | "/docs/guide/styling" | "/docs/introduction/getting-started" | "/docs/introduction/installation" | "/docs/introduction/integrations" | "/docs/introduction/welcome-to-marko" | "/docs/introduction/why-marko" | "/docs/marko-run/file-based-routing" | "/docs/marko-run/getting-started" | "/docs/marko-run/typescript" | "/docs/reference/concise-syntax" | "/docs/reference/core-tag" | "/docs/reference/custom-tag" | "/docs/reference/language" | "/docs/reference/native-tag" | "/docs/reference/reactivity" | "/docs/reference/supported-environments" | "/docs/reference/template" | "/docs/reference/typescript" | "/docs/tutorial/components-and-reactivity" | "/docs/tutorial/fundamentals"]; export type Context = Run.MultiRouteContext & Marko.Global; export type Handler = Run.HandlerLike; export type GET = Run.HandlerLike; diff --git a/docs/explanation/class-vs-tags-api.md b/docs/explanation/class-vs-tags-api.md new file mode 100644 index 00000000..ad0620e3 --- /dev/null +++ b/docs/explanation/class-vs-tags-api.md @@ -0,0 +1,242 @@ +# Tags API for Class API Developers + +> [!TLDR] +> +> - Component Boundaries are _far_ less meaningful, state lives locally +> - `class`, `state`, `out`, `component`, and other class component-specific APIs have been removed +> - Event handling is function-based instead of event-based + +Marko 4 and 5 leveraged a [Class-based API](https://v5.markojs.com/docs/class-components/) for interactivity. Marko 6 is based on a new tags-based syntax, where "everything is a tag". For developers familiar with older versions of Marko this will take some getting used to, but we are confident that the Tags API is cleaner, easier to write & read, and more concise. + +This page will discuss some of the differences in mental model between the Class API and the Tags API. + +## Components Melt Away + +The first idea to get used to when adopting the Tags API is that component boundaries have much less importance than they do in the Class API, and component-level methods no longer exist. + +In the Class API, state and lifecycle are maintained at the _component_ level. Each single-file component has its own `state`, `onInput`, `onDestroy`, and other lifecycle methods. The Tags API abandons this idea in favor of granular, compiled reactivity. State is declared with [tag variables](../reference/language.md#tag-variables) where necessary, including inside conditionals and loops, and lifecycle is attached to individual tags. + +Patterns that depend on the component instance like `getComponent` have been removed and are replaced with [local, declarative alternatives](#component-refs). + +## Updated APIs + +A quick reference for removed features and their modern equivalents: + +- `out` is no longer accessible, except `out.global` as [`$global`](../reference/language.md#global) +- [`class`](https://v5.markojs.com/docs/class-components/#single-file-components), [`state`](https://v5.markojs.com/docs/state/#state), and [`component`](https://v5.markojs.com/docs/class-components/#component) are removed in favor of [tag variables](../reference/language.md#tag-variables) +- All [instance methods](https://v5.markojs.com/docs/class-components/#methods) have been removed + - [`getEl`/`getEls`](#element-refs), [`getComponent`](#component-refs), `forceUpdate`, `subscribeTo`, etc. +- Directives are no longer necessary, as updates are more granular + - [`:scoped`](https://v5.markojs.com/docs/class-components/#scoped), [`:no-update`](https://v5.markojs.com/docs/class-components/#no-update_1), [`no-update`](https://v5.markojs.com/docs/class-components/#no-update), [`no-update-if`](https://v5.markojs.com/docs/class-components/#no-update-if), [`no-update-body`](https://v5.markojs.com/docs/class-components/#no-update-body), [`no-update-body-if`](https://v5.markojs.com/docs/class-components/#no-update-body-if) +- Mutable updates via [`replaceState`](https://v5.markojs.com/docs/class-components/#replacestatenewstate) and [`setStateDirty`](https://v5.markojs.com/docs/class-components/#setstatedirtyname-value) were always anti-patterns and have been removed intentionally with no modern alternative +- [Split components](https://v5.markojs.com/docs/class-components/#split-components) are no longer necessary, as [targeted compilation](./targeted-compilation.md) happens at the sub-component level + +## Event Handling + +Attribute arguments like `onClick("handleClick")` have been removed. Event handlers are normal attributes now, and can be used either inline or by referencing a function: + +```marko + + + + 0.5 ? "yellow" : "green"; +}> + +``` + +This _drastically_ simplifies custom tag communication, as function-based [event handlers](../reference/native-tag#event-handlers) can be passed and called directly instead of curried as events. This means they're [spreadable](../reference/language.md#spread-attributes), and [`this.emit`](https://v5.markojs.com/docs/events/#emitting-custom-events) is no longer necessary. + +```marko +/* two-buttons.marko */ +export interface Input extends Marko.HTML.Button {} + +// `onClick` and other event handlers are passed through! + + +``` + +## Element Refs + +Instead of [`getEl`](https://v5.markojs.com/docs/class-components/#getelkey-index), native tags [expose a tag variable](../reference/native-tag.md#element-references) with a getter to the DOM node. Since it is a function it can be used anywhere in the template. + +```marko + + + +``` + +> [!NOTE] +> We use `$el` by convention. The leading `$` is not necessarily required, but optimizations _may_ be added that only apply if the convention is followed. + +When references for multiple elements are required (like [`getEls`](https://v5.markojs.com/docs/class-components/#getelskey) in Marko 5), hoisted tag variables can be [iterated](../reference/language.md#repeated-tag-vars). + +```marko + + + + + + + +``` + +## Component Refs + +Since component-level operations no longer exist in the Tags API, [`getComponent`](https://v5.markojs.com/docs/class-components/#getcomponentkey-index) has been removed. Information should be passed between parent and child using [event handlers](#event-handling), the [controllable pattern](./controllable-components.md), and methods exposed by [the `` tag](../reference/core-tag.md#return). + +```marko +/* parent.marko */ + + + + +``` + +```marko +/* child.marko */ + + + + +``` + +## Lifecycle + +In the Class API, running code when something mounts inside a loop requires a child component. + +```marko +// use class + + + +``` + +```marko +/* components/log-value.marko */ +// use class +class { + onMount() { console.log(this.input.value) } +} +``` + +In the Tags API, no component boundary is required. + +```marko + + + +``` + +The Tags API has two built-in tags for lifecycle management, both of which _should be avoided unless absolutely necessary_. + +### ` +``` + +> [!CAUTION] +> The ` ``` +### Repeated Tag Vars + +When a tag variable is referenced in a hoisted context, it also implements the [iterable protocol](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterable_protocol). This enables access to a list of variables in loops or other nested scope. + +```marko + +
+ + + +``` + ## Tag Parameters While rendering [content](#tag-content), child may pass information _back_ to its parent using tag parameters. diff --git a/src/tags/app-menu/app-menu.marko b/src/tags/app-menu/app-menu.marko index fdf2e110..0003759d 100644 --- a/src/tags/app-menu/app-menu.marko +++ b/src/tags/app-menu/app-menu.marko @@ -56,6 +56,7 @@ div#site-menu class=styles.menu Page="/docs/explanation/nested-reactivity" -- Nested Reactivity Page="/docs/explanation/immutable-state" -- Immutable State Page="/docs/explanation/serializable-state" -- Serializable State + Page="/docs/explanation/class-vs-tags-api" -- Class to Tags API li strong -- Reference ul