Skip to content

feat: Marko v6 integration (@formkit/auto-animate/marko)#239

Open
defunkt-dev wants to merge 5 commits into
formkit:masterfrom
defunkt-dev:master
Open

feat: Marko v6 integration (@formkit/auto-animate/marko)#239
defunkt-dev wants to merge 5 commits into
formkit:masterfrom
defunkt-dev:master

Conversation

@defunkt-dev

@defunkt-dev defunkt-dev commented Jun 23, 2026

Copy link
Copy Markdown

Summary

Adds an official Marko v6 integration, exposed as a ./marko package export alongside the existing Vue, React, Solid, Preact, and Angular adapters. The integration is a single <auto-animate> custom tag that wraps the framework‑agnostic autoAnimate(el, options) core. This PR also includes a docs‑site framework tab and a Playwright e2e suite covering both client‑side rendering and SSR + resume.

import AutoAnimate from "@formkit/auto-animate/marko"
 
<ul/listRef>
  <for|item| of=items by="id">
    <li>${item.text}</li>
  </for>
</ul>
<AutoAnimate parent=listRef/>

With options and a reactive on/off switch:

<AutoAnimate parent=listRef options=({ duration: 200, easing: "ease-out" }) enabled=animationsOn/>

Motivation

Marko ships no built‑in animation system, so AutoAnimate fills a real gap for the common list/layout case (enter, leave, reorder of a container's children). Marko v6's reactivity is Svelte‑aligned, and the core controller is already shaped to satisfy a Svelte action, so it maps cleanly onto Marko's <lifecycle> tag with no new abstractions.

API

A single tag with three attributes — no body, no return value, no wrapper element:

  • parent: () => HTMLElement — the element whose direct children animate. The user owns the element and passes its native tag‑variable ref (e.g. <ul/listRef>parent=listRef), mirroring the FormKit drag‑and‑drop integration and the ref pattern used by the React/Vue adapters.
  • options?: Partial<AutoAnimateOptions> | AutoAnimationPlugin — read once at mount (the core has no setOptions); to change options, remount the tag.
  • enabled?: boolean — reactive; toggling it calls enable() / disable() on the controller without re‑initializing.

What's included

  • Adaptersrc/marko/auto-animate.marko (the entire integration; Input typed inline).
  • Packaging — root marko.json ({"exports": "./tags"}), a "./marko" entry in package.json exports, a marko keyword, and a markoBuild() step in build/bundle.ts that copies the source tag to dist/tags/ (the manifest is copied verbatim alongside README/LICENSE).
  • Docs — a Marko framework tab across the hero, "Dropdown", and "Disable" code switchers; a dedicated "Marko tag" usage section; left‑nav and jump‑link entries; and a four‑color IconMarko brand mark.
  • Tests — Playwright e2e for CSR (tests/marko/) and SSR + resume (tests/marko-ssr/).

Design notes (for reviewers)

  • <lifecycle> wrapper. onMount calls autoAnimate(parent(), options) once and stores the controller on the lifecycle this; onUpdate flips enable()/disable() behind a guard; onDestroy calls controller.destroy(). Teardown is also wired to the scope's AbortSignal, so an enclosing <if> going false or a <for> item being removed cleans up the observers.
  • Controller on this, not in a <let>. It's created client‑only in onMount, so it never reaches the serializer. options are static at mount by design (matches the core and the other adapters).
  • Distributed as source. Rollup can't compile .marko, and Marko tags are compiled by the consumer's @marko/vite. The build therefore copies the .marko source into dist; tag discovery is handled by the committed root marko.json exports field. The tag's relative ../index import is left extensionless and resolves to dist/index.mjs for consumers.
  • client import for the core. Keeps the DOM‑only core out of the server bundle.
  • SSR‑safe by construction. <lifecycle> emits no HTML and never runs on the server; on resume the tag reads its parent as a real DOM node via the ref (not a value produced by another effect), so there's no resume‑ordering dependency. Plain options objects survive serialization; plugin‑function options are a client‑only path (documented).
  • Keying. Reorderable <for> lists must key by identity so nodes move (FLIP) rather than being recreated — by="id" for objects, by=(n => String(n)) for primitives.

Testing

The adapter's repo uses Playwright (no vitest), and animations are timing/visual‑sensitive, so coverage is e2e. Two self‑contained harnesses are added so they don't touch the Vue docs build.

CSR (tests/marko/) — a four‑section App.marko driven by Playwright. Covers: add / remove / reorder with FLIP, node‑identity survival across reorder (keyed <for>), the reactive enabled toggle (no re‑init), the all‑removed textContent="" fast path plus re‑add, rapid overlapping mutations, primitive‑keyed lists, conditional/co‑located parent mount‑unmount, reactive‑unmount teardown via an enclosing <if>, and prefers-reduced-motion.

SSR + resume (tests/marko-ssr/) — a small render+resume server. Asserts the list is server‑rendered, the page resumes, server‑rendered children do not animate on load, a post‑resume mutation does animate, a plain options object survives resume, and characterizes an inline‑plugin options route.

How to run (from the repo root):

pnpm add -D marko@^6 @marko/compiler@^5 @marko/vite@^5.4.10 @marko/type-check
pnpm exec playwright install chromium
 
# CSR
pnpm exec playwright test --config tests/marko/playwright.config.ts --project=chromium
# SSR + resume
pnpm exec playwright test --config tests/marko-ssr/playwright.config.ts
# type-check the tag + harness
npx @marko/type-check -p tsconfig.marko.json

Note: @marko/vite is pinned to the 5.4.x line because it runs on the repo's existing Vite 7 (its major version tracks Vite compatibility, not the Marko version, and it still compiles Marko 6 tags). The shipped adapter has no build‑time Marko dependency — these are devDependencies for the test harness only.

Docs notes

Consistent with the existing site, the live <ActualMarkoApp> demo is a Vue stand‑in built on the Vue adapter — exactly like ActualReactApp.vue and the other framework demos. The .marko example files are imported as raw text (?raw) and shown as code only, so nothing Marko enters the docs' vite‑ssg build. The framework‑specific content is the code samples; the running previews share the same core.

Out of scope (possible follow‑ups)

  • App‑level defaults / a single global "animations enabled" toggle (as Vue's plugin and Solid's directive factory expose). Deliberately omitted for parity — the FormKit drag‑and‑drop integration shipped without it — and is a small additive follow‑up if wanted.
  • First‑class plugin options under SSR (would require the consumer to apply the core directly in their own client‑imported <lifecycle>).

Checklist

  • Adapter implemented as a single .marko tag and exposed via ./marko
  • Packaging: marko.json, package.json export + keyword, bundle.ts copy step
  • CSR e2e suite green
  • SSR + resume e2e suite green
  • .marko sources type‑check clean (@marko/type-check)
  • Docs: framework tab, usage section, nav/jump‑links, brand icon

@vercel

vercel Bot commented Jun 23, 2026

Copy link
Copy Markdown

@defunkt-dev is attempting to deploy a commit to the Formkit Team on Vercel.

A member of the Team first needs to authorize it.

@defunkt-dev

Copy link
Copy Markdown
Author

@justin-schroeder can you pls help us with this when you get a moment? Thanks in advance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant