Migrating from Vue to Avenx-JS
This guide details how to migrate applications built with Vue 2 / Vue 3 to Avenx-JS.
1. Architectural Overview & Mental Model Shift
Section titled “1. Architectural Overview & Mental Model Shift”Vue single file components (.vue) encapsulate <template>, <script>, and <style scoped> in one file. Avenx-JS separates templates/logic into .component.js companion files and styling into .component.css, utilizing top-level compiler tags for state and computed properties.
| Concept | Vue | Avenx-JS |
|---|---|---|
| Component Format | Single File Component (.vue) |
Companion files (.component.js + .component.css) |
| Reactivity | ref() / reactive() |
Top-level <state key="val" /> Proxy tag |
| Computed Values | computed(() => fn) |
<computed name="x" value="..." /> tag |
| Loops & Directives | v-for, v-model, v-show, v-if |
<@for>, data-ax-bind, data-ax-show, ternary HTML |
| Global Store | Pinia (defineStore) / Vuex |
Bridges (AvenxBridge in src/global/*.bridge.js) |
2. Component Structure and Templates
Section titled “2. Component Structure and Templates”A Vue SFC wraps three concerns in one file: <template> (markup), <script setup> (logic), and <style scoped> (styles). Avenx-JS splits those same three concerns into companion files: a .component.js file that holds the HTML template plus top-level compiler tags for state and computed properties, and a .component.css file holding scoped styles. The template engine is plain HTML with a few compiler tags — see Templates for the full language, and the Migration Overview for where companion files sit in the paradigm map.
SFC to Companion File Mapping
Section titled “SFC to Companion File Mapping”Vue SFC (.vue) |
Avenx-JS |
|---|---|
<template> |
.component.js — the template IS the file’s HTML |
<script setup> |
.component.js — data-props-* attributes, <state />, <computed />, <@for>, actions |
<style scoped> |
.component.css — <@css> named blocks bound via the @css attribute |
defineProps([...]) |
data-props-* attributes on the component tag in the parent; read via this.props.* |
Before — Vue Single File Component (.vue)
Section titled “Before — Vue Single File Component (.vue)”<template> <div class="list-container"> <header> <slot name="header">Default Header</slot> </header> <ul> <li v-for="(item, idx) in items" :key="item.id"> <span>{{ idx + 1 }}. {{ item.name }}</span> </li> </ul> </div></template>
<script setup>defineProps(['items']);</script>
<style scoped>.list-container { padding: 1rem; }</style>After — Avenx.js Companion Files
Section titled “After — Avenx.js Companion Files”<div class="list-container" @css container> <header> <slot name="header">Default Header</slot> </header> <ul> <@for item in this.props.items key="item.id"> <li> <span>{{ index + 1 }}. {{ item.name }}</span> </li> </@for> </ul></div><@css> container { padding: 1rem; }</@css>Note the three changes that matter:
- The template is the file. There is no
<template>wrapper — the component’s root element is whatever the.component.jsfile returns. - Styles are scoped by name.
<@css>blocks are extracted, hashed into unique class suffixes, and bound to elements with the@cssattribute (see Styling). - Props come through
this.props. The parent passesitemswithdata-props-items="...", and the child readsthis.props.items— nodefinePropsdeclaration needed.
Loops: v-for → <@for>
Section titled “Loops: v-for → <@for>”Vue’s v-for is an element directive; Avenx’s <@for> is a compiler tag that wraps the repeated block. Loop blocks are translated to <template> tags and managed by the ListManager for efficient DOM list updates.
Before — Vue v-for
Section titled “Before — Vue v-for”<ul> <li v-for="(item, idx) in items" :key="item.id"> <span>{{ idx + 1 }}. {{ item.name }}</span> </li></ul>After — Avenx <@for>
Section titled “After — Avenx <@for>”<ul> <@for item in this.props.items key="item.id"> <li> <span>{{ index + 1 }}. {{ item.name }}</span> </li> </@for></ul>The Implicit index Variable
Section titled “The Implicit index Variable”Every <@for> loop automatically injects a zero-indexed index variable into the loop template scope — ListManager adds it for you on each iteration. You never declare it:
<@for item in this.props.items key="item.id"> <span>{{ index + 1 }}. {{ item.name }}</span></@for>Slots: Default and Named
Section titled “Slots: Default and Named”Avenx supports both Vue-style default and named slots. The child component declares a <slot> element (with a fallback body); the parent supplies content with a slot="name" attribute. If the parent provides no content for a slot, the child’s fallback content renders instead.
Before — Vue Named Slot
Section titled “Before — Vue Named Slot”<div class="card"> <header> <slot name="header">Default Header</slot> </header> <main> <slot></slot> </main></div><Card> <h2 slot="header">Special Title</h2> <p>This content goes into the default slot!</p></Card>After — Avenx Slots
Section titled “After — Avenx Slots”<div class="card" @css card> <header> <slot name="header">Default Header</slot> </header> <main> <slot></slot> </main></div><!-- Parent template --><Card> <h2 slot="header">Special Title</h2> <p>This content goes into the default slot!</p></Card>Named and default slot markup is identical between Vue and Avenx — only the file layout changes.
Checking Slot Presence (this.$slots.has())
Section titled “Checking Slot Presence (this.$slots.has())”A component can decide whether the parent actually supplied content for a slot using this.$slots.has(slotName) inside an action or logic block. This is the direct replacement for Vue’s this.$slots.header checks:
<div class="card"> <h2 data-ax-show="this.$slots.has('header')">This card has a custom header</h2> <slot name="header">Default Header</slot></div>this.$slots.has() returns true only when the parent passed matching content, letting you conditionally render fallback UI without emitting empty containers — pair it with data-ax-show (Avenx’s conditional-visibility directive, the v-if/v-show replacement) as above. See Slots and $slots.has() in the Templates guide for the full details.
3. Directives and Event Handling
Section titled “3. Directives and Event Handling”This section will document translating Vue directives (v-model, v-show, :class, :style) and event syntax (@click) to Avenx.
4. Reactivity, Ref, and Computed
Section titled “4. Reactivity, Ref, and Computed”Vue 3 manages reactive state with Composition API primitives (ref(), reactive(), computed()) or the Options API (data(), computed). Avenx-JS unifies reactive state and derived values into two top-level compiler tags: one <state /> tag holds every reactive property, and <computed /> tags declare derived values. There are no .value wrappers and no setters — state is a reactive Proxy you mutate directly, and the template re-evaluates for you. See Reactive State for the full model and Computed Properties for derivations, plus the Migration Overview for where this sits in the paradigm map.
Replacing ref() and reactive() with One <state /> Tag
Section titled “Replacing ref() and reactive() with One <state /> Tag”Every Vue ref and reactive object collapses into one <state /> tag at the top of the component — each attribute is one reactive property. The tag sits in the .component.js file, is parsed at compile time, and is stripped before the class is emitted.
Before — Vue 3 Composition API (ref / reactive)
Section titled “Before — Vue 3 Composition API (ref / reactive)”import { ref, reactive } from 'vue';
const count = ref(0);const user = reactive({ name: 'Jane', role: 'admin' });
function increment() { count.value++;}After — One Avenx <state /> Tag
Section titled “After — One Avenx <state /> Tag”<state count="0" user='{"name": "Jane", "role": "admin"}' />
<action name="increment"> state.count++;</action>
<div> <p>Count: {{ state.count }}</p> <p>User: {{ state.user.name }} ({{ state.user.role }})</p> <button @click="increment()">Increment</button></div>Attribute values are coerced to their JavaScript types — numbers, booleans, arrays, and objects all work. @click="increment()" calls the action defined by the <action> tag (see Events).
No .value Unwrapping
Section titled “No .value Unwrapping”Vue’s ref() returns a wrapper object, so script blocks read count.value and write count.value = 1. Avenx state properties are accessed directly — state.count, not state.count.value — because state is already the reactive Proxy. The same is true in expressions: {{ state.count }}, not {{ state.count.value }}.
JSON Attribute Rules for Objects and Arrays
Section titled “JSON Attribute Rules for Objects and Arrays”<state /> attributes are evaluated as JSON/JavaScript expressions. The one rule to remember: object and array values must be valid JSON strings — wrap them in single quotes and use double quotes inside:
<state user='{"name": "Jane", "role": "admin"}' tags='["student", "verified"]' />This is the one formatting difference Vue developers hit most: bare { name: "Jane" } or single quotes inside the value are not valid JSON, and the attribute will not parse as you expect.
Replacing computed() with <computed />
Section titled “Replacing computed() with <computed />”A Vue computed(() => expr) becomes a <computed name="..." value="..." /> tag. The value attribute is a stringified JavaScript expression that can reference state properties and other computed names.
Before — Vue computed()
Section titled “Before — Vue computed()”import { ref, reactive, computed } from 'vue';
const count = ref(0);const user = reactive({ name: 'Jane', role: 'admin' });
const doubleCount = computed(() => count.value * 2);const greeting = computed(() => `Hello ${user.name}, count is ${count.value}`);After — Avenx <computed /> Tags
Section titled “After — Avenx <computed /> Tags”<state count="0" user='{"name": "Jane", "role": "admin"}' /><computed name="doubleCount" value="state.count * 2" /><computed name="greeting" value="'Hello ' + state.user.name + ', count is ' + state.count" />
<div> <p>{{ greeting }} (Double: {{ doubleCount }})</p></div>Template literals like Vue’s `Hello ${user.name}` become string concatenation in the value expression (template literals inside an HTML attribute are awkward to escape); the computed tag accepts any stringified JS expression, so concatenation works cleanly.
No Vue Watchers
Section titled “No Vue Watchers”Vue’s watch(), watchEffect(), and the Options API watch option have no direct counterpart in Avenx. You do not subscribe to changes — every mutation of state re-renders the template automatically, and <computed /> covers the “react to a state change with a derived value” case that watch is often used for.
// Vue: imperative side effect on changewatch(count, (next) => console.log('count is', next));<!-- Avenx: derived value declared once; the template re-evaluates on state change --><computed name="countLog" value="'count is ' + state.count" />If you genuinely need to run a side effect when a value changes, Avenx does offer this.$watch(source, callback, options) as an advanced, explicit tool — but it is an Avenx API, not Vue’s watch, and most Vue watch usages (derived UI state, logging, syncing) are better expressed as <computed /> or as work inside the action that mutates the state. See Watchers in the Reactivity guide.
5. Global Stores & Pinia to Bridges
Section titled “5. Global Stores & Pinia to Bridges”This section will document migrating Pinia stores to AvenxBridge classes.