<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged vue</title>
<link>https://coderlegion.com/tag/vue</link>
<description>Powered by Question2Answer</description>
<item>
<title>Headless Vue + WordPress Boilerplate v2.0.0</title>
<link>https://coderlegion.com/32437/headless-vue-wordpress-boilerplate-v2-0-0</link>
<description>&lt;h3 id=&quot;headless-vue-wordpress-boilerplate-v2-0-0&quot;&gt;Headless Vue + WordPress Boilerplate v2.0.0&lt;/h3&gt;
&lt;p&gt;Yes, Version 2.0.0 is here!!!&lt;/p&gt;
&lt;p&gt;This boilerplate allows you to use Vue as the front-end for your WordPress theme in a headless architecture. WordPress handles the backend and REST API, while Vue powers the user interface using modern frontend tooling like Vite.&lt;/p&gt;
&lt;h3 id=&quot;key-features&quot;&gt;Key Features&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Use Vue 3 with Vite for a fast and modern development experience&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;⚡ Hot Module Replacement inside WordPress: save a &lt;code&gt;.vue&lt;/code&gt; file and see it instantly, no rebuild&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;One-line REST API routes in PHP and one-line calls in Vue (&lt;code&gt;useApi&lt;/code&gt;, &lt;code&gt;useWp&lt;/code&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;WordPress pages, posts and menus work out of the box with Vue Router&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Modular folder structure for components, pages, APIs, and stores&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;⚙️ Supports Vue Router and Pinia (Vue Store)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Style with Tailwind CSS, standard CSS or your preferred CSS preprocessor&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;requirements&quot;&gt;Requirements&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;WordPress 6.7+ and PHP 8.1+&lt;/li&gt;
&lt;li&gt;Node.js 20+&lt;/li&gt;
&lt;li&gt;Pretty permalinks (Settings &amp;gt; Permalinks, anything except &quot;Plain&quot;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;folder-structure&quot;&gt;Folder Structure&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;themes/
└── vue-boilerplate/
    ├── functions/               # WordPress PHP functions
    │   ├── api.php              # Your REST API routes
    │   ├── api-helpers.php      # vue_theme_api_get(), vue_theme_api_post(), ...
    │   ├── vite.php             # Loads the Vue app (dev server or build)
    │   └── theme-settings.php   # Theme supports and menu locations
    ├── src/                     # Vue application root
    │   ├── api/                 # API client (index.js) and JS modules for API calls
    │   │   └── wp/
    │   ├── assets/              # Static assets like images and SVGs
    │   ├── components/          # Vue components (e.g., layout)
    │   ├── composables/         # useApi and useWp
    │   ├── page/                # Vue page views
    │   ├── router/              # Vue Router setup
    │   ├── stores/              # Pinia stores
    │   │   └── wp/
    │   ├── utils/               # HTML sanitizer and link helpers
    │   ├── App.vue              # Root Vue component
    │   ├── main.js              # Vue app entry point
    │   ├── settings.js          # Values passed from WordPress
    │   └── style.css            # Global styles
    ├── dist/                    # Production build (created by npm run build)
    ├── *.php                    # WordPress theme files (header.php, footer.php, etc.)
    ├── style.css                # Theme metadata
    └── vite.config.js           # Configuration file for Vite, a fast frontend build tool.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;getting-started&quot;&gt;Getting Started&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Download and put the files inside your &lt;code&gt;themes&lt;/code&gt; folder, then activate the theme.&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/chrischase011/wordpress-vue-theme-boilerplate/releases&quot;&gt;Download Latest Release&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Install Dependencies&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Run the Development Server&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Open &lt;strong&gt;your WordPress site URL&lt;/strong&gt; (e.g. &lt;code&gt;http://mysite.local&lt;/code&gt;), not the Vite URL.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;While &lt;code&gt;npm run dev&lt;/code&gt; is running, WordPress loads the app from the Vite dev server. Vue and CSS changes show up instantly, and PHP changes reload the page. Stop the dev server and WordPress goes back to the production build.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Make sure WordPress is running locally (e.g., with LocalWP or Laragon) and that the theme is activated.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;build-for-production&quot;&gt;Build for Production&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm run build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will compile and optimize your Vue app into &lt;code&gt;dist/&lt;/code&gt;. Upload the theme with the &lt;code&gt;dist&lt;/code&gt; folder, you don't need &lt;code&gt;node_modules&lt;/code&gt; or &lt;code&gt;src&lt;/code&gt; on the server.&lt;/p&gt;
&lt;h3 id=&quot;working-with-the-api&quot;&gt;Working with the API&lt;/h3&gt;
&lt;h4 id=&quot;add-a-route-php-&quot;&gt;Add a route (PHP)&lt;/h4&gt;
&lt;p&gt;Add it at the bottom of &lt;code&gt;functions/api.php&lt;/code&gt;. Routes live under &lt;code&gt;/wp-json/vue-theme/v1/&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;vue_theme_api_get('/books', function () {
    return get_posts(['post_type' =&amp;gt; 'book']);
});

// URL parameter
vue_theme_api_get('/books/{id}', function ($request) {
    $book = get_post((int) $request['id']);

    if (!$book) {
        return new WP_Error('not_found', 'Book not found.', ['status' =&amp;gt; 404]);
    }

    return $book;
});

// JSON body
vue_theme_api_post('/contact', function ($request) {
    $email = sanitize_email($request['email']);

    return ['sent' =&amp;gt; true];
});

// Logged in users only, or users with a capability
vue_theme_api_post('/notes', 'my_callback', ['permission' =&amp;gt; true]);
vue_theme_api_delete('/notes/{id}', 'my_callback', ['permission' =&amp;gt; 'edit_posts']);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;use-it-vue-&quot;&gt;Use it (Vue)&lt;/h4&gt;
&lt;p&gt;In a component, &lt;code&gt;useApi&lt;/code&gt; and &lt;code&gt;useWp&lt;/code&gt; are auto-imported and give you reactive state:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup&amp;gt;
const { data: books, loading, error } = useApi('books')

// WordPress core API: posts, pages, categories, media, your custom post types...
const page = ref(1)
const { data: posts, totalPages } = useWp('posts', () =&amp;gt; ({ per_page: 6, page: page.value }))
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
    &amp;lt;p v-if=&quot;loading&quot;&amp;gt;Loading...&amp;lt;/p&amp;gt;
    &amp;lt;p v-else-if=&quot;error&quot;&amp;gt;{{ error }}&amp;lt;/p&amp;gt;
    &amp;lt;ul v-else&amp;gt;
        &amp;lt;li v-for=&quot;book in books&quot; :key=&quot;book.ID&quot;&amp;gt;{{ book.post_title }}&amp;lt;/li&amp;gt;
    &amp;lt;/ul&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Anywhere else (stores, event handlers), use the clients:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;import { api, wp } from '@/api' // or a relative path like '../api'

const books = await api.get('books')
const result = await api.post('contact', { email, message })
const posts = await wp.get('posts', { per_page: 5, search: 'vue' })
const { data, total, totalPages } = await wp.paginate('posts', { page: 2 })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Failed requests throw an &lt;code&gt;ApiError&lt;/code&gt; with &lt;code&gt;message&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt; and &lt;code&gt;code&lt;/code&gt;. The nonce for logged in users is sent automatically.&lt;/p&gt;
&lt;h3 id=&quot;routing&quot;&gt;Routing&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Add your own routes in &lt;code&gt;src/router/index.js&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Any other URL is looked up in WordPress: &lt;code&gt;src/page/Content.vue&lt;/code&gt; shows the page or post with that slug, or a &quot;Page not found&quot; message.&lt;/li&gt;
&lt;li&gt;Menus assigned to &lt;strong&gt;Primary Menu&lt;/strong&gt; and &lt;strong&gt;Footer Menu&lt;/strong&gt; (Appearance &amp;gt; Menus) are rendered by &lt;code&gt;Header.vue&lt;/code&gt; and &lt;code&gt;Footer.vue&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;integration-tips&quot;&gt;Integration Tips&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Site title, logo and menus are available without a request: &lt;code&gt;useSiteStore()&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Pass your own PHP data to Vue with the &lt;code&gt;vue_theme_app_config&lt;/code&gt; filter, then read it from &lt;code&gt;window.wpVueTheme&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Always use &lt;code&gt;sanitizeHtml()&lt;/code&gt; from &lt;code&gt;src/utils/html.js&lt;/code&gt; before rendering WordPress content with &lt;code&gt;v-html&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Import images from &lt;code&gt;src/assets&lt;/code&gt; so they get the right URL after the build&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Set &lt;code&gt;show_in_rest&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt; on your custom post types to load them with &lt;code&gt;useWp('your-post-type')&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-troubleshooting&quot;&gt;❓ Troubleshooting&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&quot;The Vue app is not built yet&quot;&lt;/strong&gt;: run &lt;code&gt;npm install&lt;/code&gt; and &lt;code&gt;npm run build&lt;/code&gt;, or start &lt;code&gt;npm run dev&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Changes don't show up&lt;/strong&gt;: check that &lt;code&gt;npm run dev&lt;/code&gt; is still running and that you opened the WordPress URL.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Port 5173 is already in use&lt;/strong&gt;: create &lt;code&gt;.env.local&lt;/code&gt; with &lt;code&gt;VITE_DEV_PORT=5174&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;*&lt;em&gt;Local domain is not &lt;code&gt;localhost&lt;/code&gt;, `&lt;/em&gt;.local&lt;code&gt;, &lt;/code&gt;&lt;em&gt;.loc&lt;code&gt; or &lt;/code&gt;&lt;/em&gt;.test&lt;code&gt;**: add &lt;/code&gt;VITE_WP_ORIGIN=&lt;a rel=&quot;nofollow&quot; href=&quot;http://your-domain`&quot;&gt;http://your-domain`&lt;/a&gt; to &lt;code&gt;.env.local&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WordPress runs in Docker&lt;/strong&gt;: PHP can't see the dev server on your machine, so add &lt;code&gt;add_filter('vue_theme_check_dev_server', '__return_false');&lt;/code&gt; to &lt;code&gt;functions.php&lt;/code&gt; while developing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vue routes return a server 404&lt;/strong&gt;: enable pretty permalinks in Settings &amp;gt; Permalinks.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-upgrading-from-1-x&quot;&gt;⬆️ Upgrading from 1.x&lt;/h3&gt;
&lt;p&gt;See &lt;a rel=&quot;nofollow&quot; href=&quot;CHANGELOG.md&quot;&gt;CHANGELOG.md&lt;/a&gt; for the breaking changes.&lt;/p&gt;
&lt;h3 id=&quot;github-repo&quot;&gt;Github Repo&lt;/h3&gt;
&lt;p&gt;See &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/chrischase011/wordpress-vue-theme-boilerplate&quot;&gt;Headless Vue + WordPress Boilerplate v2.0.0&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;license&quot;&gt;License&lt;/h3&gt;
&lt;p&gt;See &lt;a rel=&quot;nofollow&quot; href=&quot;LICENSE&quot;&gt;MIT License&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Prepared by: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/chrischase011&quot;&gt;Christopher Robin Chase&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32437/headless-vue-wordpress-boilerplate-v2-0-0</guid>
<pubDate>Mon, 26 Oct 2026 01:04:13 +0000</pubDate>
</item>
<item>
<title>I remade Hamurabi, the game I used to play a lot as a kid</title>
<link>https://coderlegion.com/28118/i-remade-hamurabi-the-game-i-used-to-play-a-lot-as-a-kid</link>
<description>&lt;p&gt;Back in the 90s, when I was in school, I had a game on my home PC called Gospodar. The idea was simple: you have 10 years to rule. You plant crops, buy land, feed your people, and wait for new settlers. I could play it for hours.&lt;/p&gt;
&lt;p&gt;My dad was totally into it too. He quickly figured out the math and could sit there managing the kingdom for ages. We would often discuss how much grain to save for planting and how much to give to the people.&lt;/p&gt;
&lt;p&gt;Years later, I found out it was actually the classic game Hamurabi. The code was written in BASIC back in 1973, and the original dates all the way back to 1968! The developers of the version I played just changed the name and the setting, but kept the original rules.&lt;/p&gt;
&lt;p&gt;I always had warm memories about it and wanted to play it again. I tried finding modern versions online, but they just weren't fun. They were either pure text or had broken logic that made it way too easy to win.&lt;/p&gt;
&lt;p&gt;I usually build websites and apps, but this time I thought, why not recreate this game from scratch exactly the way I want it?&lt;/p&gt;
&lt;p&gt;I decided to stick to the original, keeping the Hamurabi name and the Babylon setting. I found the classic formulas online, built the engine, ran it, and realized something. My childhood memory was actually a pretty simple game. When you are an adult, those 10 years don't feel like a lifetime anymore, and the mechanics are too obvious.&lt;/p&gt;
&lt;p&gt;I played it many times and figured out what needed to change. The original game was a bit repetitive and the math was too simple. You could get the highest score just by getting lucky in the final year.&lt;/p&gt;
&lt;p&gt;So, I made the math a bit harder. Now, just saving up grain until year 10 is not enough for a good ending. The game tracks the stability of your rule over the whole period. It checks how population growth matches your land expansion, if there were any sudden crashes, and how well you handled crises. The cost of a mistake is much higher now.&lt;/p&gt;
&lt;p&gt;Winning is not easy anymore. But it is possible, even if you accidentally starve a few dozen people along the way (tested it myself 😅).&lt;/p&gt;
&lt;p&gt;Aside from the logic, I also updated the visuals. I generated some graphics and music, and added a few languages. It is a web app, so you can play it in any browser on your computer or phone. There are no registrations or accounts. Your progress is automatically saved right in your browser, so you can stop anytime and continue later.&lt;/p&gt;
&lt;p&gt;I hope those who remember the classic Hamurabi get a nice feeling of nostalgia. And if you have never heard of it, this is a great chance to touch a piece of gaming history.&lt;/p&gt;
&lt;p&gt;You can try your hand at ruling Babylon here: &lt;a rel=&quot;nofollow&quot; href=&quot;https://hammurabi.webixsoft.com&quot;&gt;https://hammurabi.webixsoft.com&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28118/i-remade-hamurabi-the-game-i-used-to-play-a-lot-as-a-kid</guid>
<pubDate>Tue, 22 Sep 2026 08:07:05 +0000</pubDate>
</item>
<item>
<title>I Kept Rebuilding PDF Features in Vue 3, So I Built vue-pdf-export</title>
<link>https://coderlegion.com/26179/i-kept-rebuilding-pdf-features-in-vue-3-so-i-built-vue-pdf-export</link>
<description>&lt;p&gt;Hey everyone 👋&lt;/p&gt;
&lt;p&gt;I recently released &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/vue-pdf-export&quot;&gt;&lt;code&gt;vue-pdf-export&lt;/code&gt;&lt;/a&gt;, an open-source Vue 3 + TypeScript library for generating PDFs directly in the browser.&lt;/p&gt;
&lt;p&gt;I built it after repeatedly needing the same PDF features across different Vue projects and wanting one reusable solution instead of rebuilding the same logic each time.&lt;/p&gt;
&lt;h3 id=&quot;what-it-supports&quot;&gt;What it supports&lt;/h3&gt;
&lt;p&gt;Some of the main features include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Canvas and selectable-text PDF rendering&lt;/li&gt;
&lt;li&gt;Automatic and manual pagination&lt;/li&gt;
&lt;li&gt;Preview, download, and browser print&lt;/li&gt;
&lt;li&gt;Headers and footers&lt;/li&gt;
&lt;li&gt;Watermarks and page targeting&lt;/li&gt;
&lt;li&gt;Clickable PDF links&lt;/li&gt;
&lt;li&gt;Password protection and permissions&lt;/li&gt;
&lt;li&gt;PDF metadata and compression&lt;/li&gt;
&lt;li&gt;Progress reporting and cancellation&lt;/li&gt;
&lt;li&gt;Large-document rendering&lt;/li&gt;
&lt;li&gt;Blob, ArrayBuffer, Base64, and PDF data URL outputs&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data-pdf-ignore&lt;/code&gt; for excluding UI elements from exports&lt;/li&gt;
&lt;li&gt;Export-only page and body styling&lt;/li&gt;
&lt;li&gt;Nuxt 3 and Nuxt 4 SSR-safe package imports&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The package also has compatibility coverage for npm, pnpm, Yarn, Yarn Plug'n'Play, and Bun.&lt;/p&gt;
&lt;h3 id=&quot;installation&quot;&gt;Installation&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install vue-pdf-export
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;basic-usage&quot;&gt;Basic usage&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
import { ref } from 'vue'
import { Html2Pdf } from 'vue3-pdf-export'
import 'vue3-pdf-export/style.css'

const pdf = ref&amp;lt;InstanceType&amp;lt;typeof Html2Pdf&amp;gt; | null&amp;gt;(null)

async function generate() {
  await pdf.value?.generatePdf()
}
&amp;lt;/script&amp;gt;

&amp;lt;template&amp;gt;
  &amp;lt;button @click=&quot;generate&quot;&amp;gt;
    Generate PDF
  &amp;lt;/button&amp;gt;

  &amp;lt;Html2Pdf
    ref=&quot;pdf&quot;
    filename=&quot;example.pdf&quot;
    :preview-modal=&quot;true&quot;
  &amp;gt;
    &amp;lt;template #pdf-content&amp;gt;
      &amp;lt;main&amp;gt;
        &amp;lt;h1&amp;gt;Hello from Vue&amp;lt;/h1&amp;gt;
        &amp;lt;p&amp;gt;This content will be exported to PDF.&amp;lt;/p&amp;gt;
      &amp;lt;/main&amp;gt;
    &amp;lt;/template&amp;gt;
  &amp;lt;/Html2Pdf&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;canvas-or-selectable-text&quot;&gt;Canvas or selectable text&lt;/h3&gt;
&lt;p&gt;Use Canvas rendering when HTML/CSS fidelity is the priority:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;Html2Pdf render-mode=&quot;canvas&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or use selectable-text rendering when users need to select, search, and copy text from the generated PDF:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;Html2Pdf render-mode=&quot;text&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This makes the package useful for both highly visual layouts and text-heavy documents such as reports, invoices, statements, and documentation.&lt;/p&gt;
&lt;p&gt;The idea is to let developers choose between maximum browser-style fidelity and real selectable PDF text depending on their use case.&lt;/p&gt;
&lt;h3 id=&quot;programmatic-output&quot;&gt;Programmatic output&lt;/h3&gt;
&lt;p&gt;The generated PDF can also be used directly in your application:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const blob = await pdf.value?.generatePdf()
const buffer = await pdf.value?.generatePdfArrayBuffer()
const base64 = await pdf.value?.generatePdfBase64()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is useful when you want to upload, store, preview, or pass the PDF to another API instead of immediately downloading it.&lt;/p&gt;
&lt;h3 id=&quot;nuxt-support&quot;&gt;Nuxt support&lt;/h3&gt;
&lt;p&gt;The package is designed so importing it does not crash Nuxt SSR.&lt;/p&gt;
&lt;p&gt;Actual PDF generation still happens in the browser, so a typical Nuxt integration can use &lt;code&gt;ClientOnly&lt;/code&gt; or lazy client-side rendering when needed.&lt;/p&gt;
&lt;h3 id=&quot;pagination&quot;&gt;Pagination&lt;/h3&gt;
&lt;p&gt;The package supports both automatic and manual pagination.&lt;/p&gt;
&lt;p&gt;For an explicit manual page break:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;div class=&quot;html2pdf__page-break&quot;&amp;gt;&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are also options for keeping elements together and avoiding unwanted page breaks.&lt;/p&gt;
&lt;h3 id=&quot;large-document-rendering&quot;&gt;Large-document rendering&lt;/h3&gt;
&lt;p&gt;Large PDFs were one of the areas I wanted to improve significantly.&lt;/p&gt;
&lt;p&gt;Canvas rendering can become expensive when a browser attempts to create one extremely tall canvas for an entire document.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;vue3-pdf-export&lt;/code&gt; can now render paginated Canvas documents incrementally, page by page, instead of depending on one giant full-document capture.&lt;/p&gt;
&lt;p&gt;In one benchmark fixture, the previous long-document capture surface was around:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;99.96 million pixels
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;while incremental rendering kept the maximum capture surface around:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1.74 million pixels per page
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is approximately a 98.26% reduction in peak capture-surface geometry for that fixture.&lt;/p&gt;
&lt;p&gt;This is specifically a rendering-surface measurement, not a claim that total browser memory always drops by the same percentage.&lt;/p&gt;
&lt;p&gt;The current regression suite includes documents across:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;10 / 25 / 40 / 50 / 60 / 75 / 100 pages
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;50-page and 100-page Canvas documents are part of the current verified test range.&lt;/p&gt;
&lt;p&gt;100 pages is not a hard package limit. Practical limits still depend on the document, images, browser memory, rendering mode, security settings, and the device being used.&lt;/p&gt;
&lt;h3 id=&quot;cancellation&quot;&gt;Cancellation&lt;/h3&gt;
&lt;p&gt;Long-running PDF generation can now be cancelled.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;pdf.value?.cancelGeneration()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cancellation is cooperative and works across the generation pipeline.&lt;/p&gt;
&lt;p&gt;The library also tests recovery after cancellation, so a new PDF can be generated after an earlier operation was cancelled.&lt;/p&gt;
&lt;p&gt;Some already-running synchronous browser operations cannot always be stopped immediately, but the pipeline checks for cancellation between major stages.&lt;/p&gt;
&lt;h3 id=&quot;page-aware-progress&quot;&gt;Page-aware progress&lt;/h3&gt;
&lt;p&gt;Progress reporting now includes more than a simple percentage.&lt;/p&gt;
&lt;p&gt;The package can expose:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;current progress&lt;/li&gt;
&lt;li&gt;current page&lt;/li&gt;
&lt;li&gt;total pages&lt;/li&gt;
&lt;li&gt;current processing stage&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Stages include:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;pagination
images
preparing
rendering
watermark
header
footer
metadata
serializing
complete
error
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This makes it possible to build UI such as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Rendering page 18 of 50...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;instead of showing an indefinite spinner.&lt;/p&gt;
&lt;h3 id=&quot;headers&quot;&gt;Headers&lt;/h3&gt;
&lt;p&gt;Headers can contain:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;plain text&lt;/li&gt;
&lt;li&gt;trusted HTML&lt;/li&gt;
&lt;li&gt;optional logos&lt;/li&gt;
&lt;li&gt;custom backgrounds and text colors&lt;/li&gt;
&lt;li&gt;configurable logo dimensions&lt;/li&gt;
&lt;li&gt;selected-page targeting&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A header can appear on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;all pages&lt;/li&gt;
&lt;li&gt;the first page only&lt;/li&gt;
&lt;li&gt;the last page only&lt;/li&gt;
&lt;li&gt;every page except the first&lt;/li&gt;
&lt;li&gt;explicit page numbers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Selective header targeting also uses page-aware spacing, so header space is reserved only where the header is actually rendered.&lt;/p&gt;
&lt;h3 id=&quot;footers&quot;&gt;Footers&lt;/h3&gt;
&lt;p&gt;Footers support:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HTML mode&lt;/li&gt;
&lt;li&gt;text mode&lt;/li&gt;
&lt;li&gt;trusted HTML content&lt;/li&gt;
&lt;li&gt;optional logos&lt;/li&gt;
&lt;li&gt;configurable logo dimensions&lt;/li&gt;
&lt;li&gt;background and text colors&lt;/li&gt;
&lt;li&gt;page-number templates&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Page {n} of {total}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Header and footer logos preserve their aspect ratio instead of being stretched into a fixed box.&lt;/p&gt;
&lt;h4 id=&quot;footer-performance-improvements&quot;&gt;Footer performance improvements&lt;/h4&gt;
&lt;p&gt;Static HTML footers are now rendered once and reused across pages instead of being rasterized repeatedly.&lt;/p&gt;
&lt;p&gt;In project benchmark fixtures:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;HTML footer without logo
7.82s -&amp;gt; 1.43s

HTML footer with logo
11.81s -&amp;gt; 1.29s

Footer-heavy PDF size
~46.7 MB -&amp;gt; ~1.0 MB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These numbers are specific to the benchmark fixtures and should not be treated as universal performance guarantees.&lt;/p&gt;
&lt;h3 id=&quot;repeated-image-optimization&quot;&gt;Repeated image optimization&lt;/h3&gt;
&lt;p&gt;Selectable-text rendering now caches repeated images within a single generation.&lt;/p&gt;
&lt;p&gt;If the same image is reused across many pages, the package can avoid repeatedly loading and encoding the same source.&lt;/p&gt;
&lt;p&gt;The cache is scoped to one PDF generation and is cleared afterward.&lt;/p&gt;
&lt;p&gt;This is especially useful for documents containing repeated logos, icons, product images, or other shared assets.&lt;/p&gt;
&lt;h3 id=&quot;resource-cleanup&quot;&gt;Resource cleanup&lt;/h3&gt;
&lt;p&gt;A lot of work also went into cleaning temporary resources earlier.&lt;/p&gt;
&lt;p&gt;The generation pipeline now handles cleanup of things such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;temporary cloned DOM nodes&lt;/li&gt;
&lt;li&gt;temporary canvases&lt;/li&gt;
&lt;li&gt;encoded image data&lt;/li&gt;
&lt;li&gt;per-generation image caches&lt;/li&gt;
&lt;li&gt;rendering state&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Canvas backing stores are also released when possible after they are no longer needed.&lt;/p&gt;
&lt;p&gt;Repeated-generation and isolation tests were added to help detect regressions in cleanup behavior.&lt;/p&gt;
&lt;h3 id=&quot;browser-responsiveness&quot;&gt;Browser responsiveness&lt;/h3&gt;
&lt;p&gt;Large PDF generation is naturally expensive, especially in Canvas mode.&lt;/p&gt;
&lt;p&gt;The pipeline now yields between selected expensive operations so the browser gets opportunities to process UI work while generation is running.&lt;/p&gt;
&lt;p&gt;Combined with cancellation and page-aware progress, this makes long-running generation easier to integrate into real applications.&lt;/p&gt;
&lt;h3 id=&quot;watermarks&quot;&gt;Watermarks&lt;/h3&gt;
&lt;p&gt;Watermarks can be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;text or image&lt;/li&gt;
&lt;li&gt;foreground or background&lt;/li&gt;
&lt;li&gt;single or repeated&lt;/li&gt;
&lt;li&gt;rotated&lt;/li&gt;
&lt;li&gt;positioned&lt;/li&gt;
&lt;li&gt;configured with opacity&lt;/li&gt;
&lt;li&gt;targeted to selected PDF pages&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;pdf-security&quot;&gt;PDF security&lt;/h3&gt;
&lt;p&gt;You can optionally generate password-protected PDFs with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;user/open password&lt;/li&gt;
&lt;li&gt;owner password&lt;/li&gt;
&lt;li&gt;configurable permissions&lt;/li&gt;
&lt;li&gt;password construction from multiple password parts&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Supported permission controls include:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;print
modify
copy
annot-forms
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;metadata-and-compression&quot;&gt;Metadata and compression&lt;/h3&gt;
&lt;p&gt;PDF metadata can include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;title&lt;/li&gt;
&lt;li&gt;author&lt;/li&gt;
&lt;li&gt;subject&lt;/li&gt;
&lt;li&gt;keywords&lt;/li&gt;
&lt;li&gt;creator&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;jsPDF stream compression can also be enabled when needed.&lt;/p&gt;
&lt;h3 id=&quot;clickable-links&quot;&gt;Clickable links&lt;/h3&gt;
&lt;p&gt;PDF link annotations are also supported, allowing links in generated documents to remain interactive instead of becoming plain visual text.&lt;/p&gt;
&lt;h3 id=&quot;safer-filenames&quot;&gt;Safer filenames&lt;/h3&gt;
&lt;p&gt;Generated filenames are normalized before download.&lt;/p&gt;
&lt;p&gt;This includes handling invalid filename characters, whitespace, reserved Windows names, trailing dots or spaces, and overly long filenames.&lt;/p&gt;
&lt;p&gt;Unicode filenames are still preserved.&lt;/p&gt;
&lt;h3 id=&quot;a-note-about-encrypted-raster-pdfs&quot;&gt;A note about encrypted raster PDFs&lt;/h3&gt;
&lt;p&gt;While profiling larger secured documents, I found that encrypted raster PDFs could retain significantly more browser memory than equivalent unencrypted PDFs.&lt;/p&gt;
&lt;p&gt;I reproduced the behavior directly with jsPDF outside the main &lt;code&gt;vue3-pdf-export&lt;/code&gt; rendering pipeline.&lt;/p&gt;
&lt;p&gt;That means a substantial part of this behavior appears to come from the underlying PDF library rather than from the Vue component itself.&lt;/p&gt;
&lt;p&gt;I opened an upstream jsPDF issue with the reproduction and benchmark details:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/parallax/jsPDF/issues/4017&quot;&gt;https://github.com/parallax/jsPDF/issues/4017&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;For large secured documents, selectable-text mode can sometimes be a useful alternative because text-based PDFs are generally much smaller than equivalent rasterized documents.&lt;/p&gt;
&lt;p&gt;This is still workload-dependent, so applications generating large encrypted PDFs should test their own representative content.&lt;/p&gt;
&lt;h3 id=&quot;testing-and-performance-work&quot;&gt;Testing and performance work&lt;/h3&gt;
&lt;p&gt;The project now includes automated testing around:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;unit behavior&lt;/li&gt;
&lt;li&gt;Chromium&lt;/li&gt;
&lt;li&gt;Firefox&lt;/li&gt;
&lt;li&gt;WebKit&lt;/li&gt;
&lt;li&gt;large documents&lt;/li&gt;
&lt;li&gt;Canvas rendering&lt;/li&gt;
&lt;li&gt;selectable-text rendering&lt;/li&gt;
&lt;li&gt;cancellation and recovery&lt;/li&gt;
&lt;li&gt;image-heavy documents&lt;/li&gt;
&lt;li&gt;repeated images&lt;/li&gt;
&lt;li&gt;footer optimization&lt;/li&gt;
&lt;li&gt;browser cleanup&lt;/li&gt;
&lt;li&gt;security behavior&lt;/li&gt;
&lt;li&gt;memory measurements&lt;/li&gt;
&lt;li&gt;repeated generation&lt;/li&gt;
&lt;li&gt;stress testing&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The current unit suite contains:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;19 test files
106 tests
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are also dedicated benchmark and Playwright workflows for performance-heavy scenarios.&lt;/p&gt;
&lt;p&gt;For heavy PDF tests, I intentionally run browser work sequentially where appropriate so multiple workers do not distort memory measurements.&lt;/p&gt;
&lt;h3 id=&quot;why-i-built-a-vue-layer-instead-of-replacing-the-underlying-libraries&quot;&gt;Why I built a Vue layer instead of replacing the underlying libraries&lt;/h3&gt;
&lt;p&gt;The goal was never to pretend that &lt;code&gt;html2pdf.js&lt;/code&gt;, &lt;code&gt;html2canvas&lt;/code&gt;, or &lt;code&gt;jsPDF&lt;/code&gt; do not exist.&lt;/p&gt;
&lt;p&gt;Those libraries still provide important pieces of the Canvas rendering pipeline.&lt;/p&gt;
&lt;p&gt;The goal of &lt;code&gt;vue3-pdf-export&lt;/code&gt; is to provide a Vue-oriented layer around PDF generation so common production requirements do not need to be rebuilt for every application.&lt;/p&gt;
&lt;p&gt;At the same time, developers can still pass advanced html2pdf.js and jsPDF options when the higher-level API is not enough.&lt;/p&gt;
&lt;p&gt;The selectable-text renderer provides another option for cases where real PDF text matters more than exact Canvas-style browser rendering.&lt;/p&gt;
&lt;h3 id=&quot;current-release&quot;&gt;Current release&lt;/h3&gt;
&lt;p&gt;The current release is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;vue-pdf-export v1.0.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The project has grown significantly since the original release, including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Canvas and selectable-text rendering modes&lt;/li&gt;
&lt;li&gt;incremental page-by-page Canvas generation&lt;/li&gt;
&lt;li&gt;large-document regression coverage&lt;/li&gt;
&lt;li&gt;cancellation and recovery&lt;/li&gt;
&lt;li&gt;current-page / total-page progress&lt;/li&gt;
&lt;li&gt;repeated-image caching&lt;/li&gt;
&lt;li&gt;footer rendering optimization&lt;/li&gt;
&lt;li&gt;earlier DOM and Canvas cleanup&lt;/li&gt;
&lt;li&gt;browser responsiveness improvements&lt;/li&gt;
&lt;li&gt;PDF security&lt;/li&gt;
&lt;li&gt;metadata and compression&lt;/li&gt;
&lt;li&gt;clickable links&lt;/li&gt;
&lt;li&gt;trusted HTML headers and footers&lt;/li&gt;
&lt;li&gt;logo support&lt;/li&gt;
&lt;li&gt;watermarks&lt;/li&gt;
&lt;li&gt;filename normalization&lt;/li&gt;
&lt;li&gt;expanded unit, browser, stress, and performance testing&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;try-it&quot;&gt;Try it&lt;/h3&gt;
&lt;p&gt;🧪 &lt;strong&gt;Live Playground&lt;/strong&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://vue-pdf-export-playground.vercel.app/&quot;&gt;https://vue-pdf-export-playground.vercel.app/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;📖 &lt;strong&gt;Documentation&lt;/strong&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://vue-pdf-export-docs.vercel.app/&quot;&gt;https://vue-pdf-export-docs.vercel.app/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;📦 &lt;strong&gt;npm&lt;/strong&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/vue-pdf-export&quot;&gt;https://www.npmjs.com/package/vue-pdf-export&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;💬 &lt;strong&gt;Feature requests &amp;amp; feedback&lt;/strong&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://vue3-pdf-export.canny.io/&quot;&gt;https://vue3-pdf-export.canny.io/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I’d really appreciate feedback from Vue and Nuxt developers, especially around real-world PDF edge cases, API ergonomics, pagination, large documents, or features you would expect from a Vue PDF library.&lt;/p&gt;
&lt;p&gt;If you are building a Vue application that needs client-side PDF generation, I would love to hear about the kinds of documents you are generating and the edge cases you run into.&lt;/p&gt;
&lt;p&gt;The project is still evolving, and real-world feedback is one of the most useful inputs for deciding what to improve next. If you find a bug, performance cases, or have an idea for an API improvement, feel free to let me know.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/26179/i-kept-rebuilding-pdf-features-in-vue-3-so-i-built-vue-pdf-export</guid>
<pubDate>Fri, 04 Sep 2026 00:10:12 +0000</pubDate>
</item>
<item>
<title>Run Vue Component Tests Where Vue Runs: The Browser</title>
<link>https://coderlegion.com/25972/run-vue-component-tests-where-vue-runs-the-browser</link>
<description>&lt;p&gt;A Vue component's job is to produce DOM in a browser. Most component tests ask it to do that somewhere else: in Node, against a DOM that jsdom simulates. That has been the default since &lt;code&gt;npm create vue@latest&lt;/code&gt; started offering Vitest with jsdom, and for plenty of tests it is the right trade. It does set a ceiling on what a green test proves, though. Nothing is ever drawn. Your CSS never runs and nothing has a size or a position, so a component can pass every assertion in the file and still be broken on screen.&lt;/p&gt;
&lt;p&gt;I co-maintain &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/twd-js&quot;&gt;twd-js&lt;/a&gt;, which runs tests inside your actual dev server, in a sidebar, next to the app. It was built for flow testing: visit a route, click through the app, assert on what the user sees. Component testing was the thing it did not do.&lt;/p&gt;
&lt;p&gt;Then I tried calling &lt;code&gt;render()&lt;/code&gt; from &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/@testing-library/vue&quot;&gt;@testing-library/vue&lt;/a&gt; inside a TWD test.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { afterEach, describe, it } from &quot;twd-js/runner&quot;;
import { twd, userEvent } from &quot;twd-js&quot;;
import { render, screen, cleanup } from &quot;@testing-library/vue&quot;;
import HomeView from &quot;../../views/HomeView.vue&quot;;
import { componentHost, restorePage } from &quot;../support/componentHost&quot;;

describe(&quot;HomeView component&quot;, () =&amp;gt; {
  afterEach(() =&amp;gt; {
    cleanup();
    restorePage();
  });

  it(&quot;increments the counter on click&quot;, async () =&amp;gt; {
    // componentHost() is a blank div on an empty page. More on it below.
    render(HomeView, { container: componentHost() });

    const button = await screen.findByTestId(&quot;counter-button&quot;);
    twd.should(button, &quot;contain.text&quot;, &quot;Count is 0&quot;);

    await userEvent.click(button);
    twd.should(button, &quot;contain.text&quot;, &quot;Count is 1&quot;);
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nothing broke. The component mounts into the page, the sidebar shows it running, and reactivity does what reactivity does, in a browser, against a DOM nobody had to simulate.&lt;/p&gt;
&lt;h3 id=&quot;why-this-works-at-all&quot;&gt;Why this works at all&lt;/h3&gt;
&lt;p&gt;Vue Testing Library is a thin layer. &lt;code&gt;render()&lt;/code&gt; mounts your component with &lt;code&gt;@vue/test-utils&lt;/code&gt; and binds &lt;code&gt;@testing-library/dom&lt;/code&gt; queries to the result. Neither of those is tied to jsdom. jsdom is just the DOM most people hand them.&lt;/p&gt;
&lt;p&gt;Give them a real one and the same code runs, except now the component is actually on a screen, with a real size, a real position, and your CSS applied to it. TWD already runs inside your app in the browser, so the real DOM is right there.&lt;/p&gt;
&lt;h3 id=&quot;the-setup&quot;&gt;The setup&lt;/h3&gt;
&lt;p&gt;One helper, and no changes to your app.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;render()&lt;/code&gt; appends its container to &lt;code&gt;document.body&lt;/code&gt;, so the component lands after &lt;code&gt;#app&lt;/code&gt;, a full viewport below your layout. The app is also still on the page, so &lt;code&gt;screen&lt;/code&gt; matches its elements as well as the ones your test just rendered. Both of those are the same problem: the app is in the way.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const HOST_ID = &quot;twd-component-host&quot;;
const APP_ROOT_ID = &quot;app&quot;;

let appRoot: HTMLElement | null = null;
let placeholder: Comment | null = null;

export function componentHost(): HTMLElement {
  detachApp();

  let host = document.getElementById(HOST_ID);
  if (!host) {
    host = document.createElement(&quot;div&quot;);
    host.id = HOST_ID;
  }
  if (!host.isConnected) {
    document.body.prepend(host);
  }

  host.innerHTML = &quot;&quot;;
  return host;
}

export function restorePage(): void {
  document.getElementById(HOST_ID)?.remove();
  attachApp();
}

function detachApp(): void {
  if (placeholder) return;

  const root = document.getElementById(APP_ROOT_ID);
  if (!root) return;

  appRoot = root;
  placeholder = document.createComment(&quot; app detached by twd component test &quot;);
  root.replaceWith(placeholder);
}

function attachApp(): void {
  if (!placeholder || !appRoot) return;

  placeholder.replaceWith(appRoot);
  placeholder = null;
  appRoot = null;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Detaching the app root is not the same as emptying it. &lt;code&gt;app.innerHTML = &quot;&quot;&lt;/code&gt; pulls the DOM out from under Vue while its vnodes still point at those nodes, and the next router navigation patches elements that are no longer in the document. Moving the node out and putting it back leaves that correspondence intact, so &lt;code&gt;restorePage()&lt;/code&gt; returns a live app.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;prepend&lt;/code&gt; rather than &lt;code&gt;append&lt;/code&gt; puts the component at the top of the page. It also keeps it in normal flow, which matters in a browser: the page is offset to make room for the TWD sidebar, and anything in normal flow inherits that offset for free.&lt;/p&gt;
&lt;p&gt;One detail worth keeping: &lt;code&gt;cleanup()&lt;/code&gt; and &lt;code&gt;restorePage()&lt;/code&gt; belong in &lt;code&gt;afterEach&lt;/code&gt;, not &lt;code&gt;beforeEach&lt;/code&gt;. In jsdom the environment is torn down for you between files. In a real browser it is not, so renders stack up, and your flow tests need the app back before they run.&lt;/p&gt;
&lt;p&gt;That is the whole setup. Your app does not change.&lt;/p&gt;
&lt;h3 id=&quot;the-part-that-changes-how-the-test-reads&quot;&gt;The part that changes how the test reads&lt;/h3&gt;
&lt;p&gt;Here is the same idea on a view that fetches on mount and posts a form.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;it(&quot;posts the form values when a todo is created&quot;, async () =&amp;gt; {
  await twd.mockRequest(&quot;getTodoList&quot;, {
    method: &quot;GET&quot;,
    url: &quot;/api/todos&quot;,
    response: [],
    status: 200,
  });
  await twd.mockRequest(&quot;createTodo&quot;, {
    method: &quot;POST&quot;,
    url: &quot;/api/todos&quot;,
    status: 201,
    response: { id: &quot;1&quot;, title: &quot;Write the Vue post&quot;, description: &quot;In a real browser&quot;, date: &quot;2026-09-01&quot; },
  });

  render(TodosView, { container: componentHost() });
  await twd.waitForRequest(&quot;getTodoList&quot;);

  await userEvent.type(await screen.findByLabelText(&quot;Title&quot;), &quot;Write the Vue post&quot;);
  await userEvent.type(screen.getByLabelText(&quot;Description&quot;), &quot;In a real browser&quot;);
  await userEvent.type(screen.getByLabelText(&quot;Date&quot;), &quot;2026-09-01&quot;);

  await userEvent.click(screen.getByRole(&quot;button&quot;, { name: &quot;Create Todo&quot; }));

  const rule = await twd.waitForRequest(&quot;createTodo&quot;);
  expect(rule.request).to.deep.equal({
    title: &quot;Write the Vue post&quot;,
    description: &quot;In a real browser&quot;,
    date: &quot;2026-09-01&quot;,
  });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Nothing here is stubbed except the network. The component's &lt;code&gt;onMounted&lt;/code&gt;, its &lt;code&gt;ref&lt;/code&gt; state, the &lt;code&gt;v-model&lt;/code&gt; bindings and the axios call all run for real, and the assertion is on the request that actually left the browser. In jsdom that test usually starts with a &lt;code&gt;vi.mock&lt;/code&gt; of the API module, and from then on you are asserting that your component calls your mock correctly.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;userEvent&lt;/code&gt; here comes from &lt;code&gt;twd-js&lt;/code&gt;, which re-exports &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/@testing-library/user-event&quot;&gt;@testing-library/user-event&lt;/a&gt; so there is nothing extra to install. It drives &lt;code&gt;v-model&lt;/code&gt; the way a person does, one key event at a time, and in a real browser those are real key events.&lt;/p&gt;
&lt;h3 id=&quot;both-kinds-of-test-one-run&quot;&gt;Both kinds of test, one run&lt;/h3&gt;
&lt;p&gt;Component tests and flow tests are now the same kind of artifact. Files in the same project, listed in the same sidebar, running in the same browser session against the same bundle. There is no second runner, no second DOM, and no second config to keep in sync.&lt;/p&gt;
&lt;p&gt;The one thing to configure is the other direction. If you also run Vitest in the same repo, exclude the browser tests from it, or Vitest will collect them, find no &lt;code&gt;describe&lt;/code&gt; it recognises, and fail the run:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;test: {
  exclude: [...configDefaults.exclude, &quot;**/*.twd.test.*&quot;],
},
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;where-each-style-fits&quot;&gt;Where each style fits&lt;/h3&gt;
&lt;p&gt;Rendering a component in isolation is the right move when the component is the subject: a form's validation states, a modal that opens and closes, a table that sorts. You skip the navigation, you skip the fixtures, and the test says exactly what it is about.&lt;/p&gt;
&lt;p&gt;Flow tests stay the right move for anything that crosses a boundary. Routing, data loading, a sequence of screens, state that survives a navigation. Rendering a component in isolation to test those means rebuilding the app around it, which is how component test files end up longer than the components.&lt;/p&gt;
&lt;p&gt;The useful change is not that one replaced the other. It is that choosing between them is now a decision about scope, made per test, instead of a decision about which runner and which DOM you are committing to.&lt;/p&gt;
&lt;h3 id=&quot;try-it-1&quot;&gt;Try it&lt;/h3&gt;
&lt;p&gt;If you already have Vue Testing Library tests, copy one, change the imports for &lt;code&gt;describe&lt;/code&gt; and &lt;code&gt;it&lt;/code&gt; to &lt;code&gt;twd-js/runner&lt;/code&gt;, and hand &lt;code&gt;render()&lt;/code&gt; the component host. The rest of the test stays as it is.&lt;/p&gt;
&lt;p&gt;The example app is on GitHub, with the same component tested in jsdom and in the browser side by side: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/BRIKEV/twd-vue-example&quot;&gt;BRIKEV/twd-vue-example&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I wrote the React version of this first, if you want the same walkthrough with &lt;code&gt;@testing-library/react&lt;/code&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/kevinccbsg/no-more-fake-dom-testing-library-unit-tests-in-the-real-browser-3p77&quot;&gt;No More Fake DOM&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/25972/run-vue-component-tests-where-vue-runs-the-browser</guid>
<pubDate>Tue, 01 Sep 2026 07:35:19 +0000</pubDate>
</item>
<item>
<title>How to Convert Word to PDF in the Browser with Vue 3, mammoth, and html2pdf.js</title>
<link>https://coderlegion.com/20522/how-to-convert-word-to-pdf-in-the-browser-with-vue-3-mammoth-and-html2pdf-js</link>
<description>&lt;p&gt;Converting Word documents to PDFs on the server is the classic approach: upload the file, run LibreOffice or a cloud API, send the result back. But that means your users’ resumes, contracts, and reports touch your infrastructure.&lt;/p&gt;
&lt;p&gt;I wanted something simpler for &lt;a rel=&quot;nofollow&quot; href=&quot;https://en.sotool.top&quot;&gt;en.sotool.top&lt;/a&gt;: pick a .docx file in the browser, preview the parsed content, and download a PDF. No server involved.&lt;/p&gt;
&lt;p&gt;Here is how I built it with Vue 3, &lt;code&gt;mammoth&lt;/code&gt;, and &lt;code&gt;html2pdf.js&lt;/code&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-client-side-&quot;&gt;Why Client-Side?&lt;/h3&gt;
&lt;p&gt;The main reason is privacy. Resumes, contracts, tax documents — users do not want them on a stranger’s server. Client-side conversion also means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No upload bandwidth limits&lt;/li&gt;
&lt;li&gt;No file size caps from your server&lt;/li&gt;
&lt;li&gt;No storage to clean up&lt;/li&gt;
&lt;li&gt;Works offline after the page loads&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The trade-off is that very complex documents are limited by the browser’s rendering capabilities. For typical office documents, that is fine.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-stack&quot;&gt;The Stack&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Vue 3&lt;/strong&gt; — UI, file handling, and reactive state&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;mammoth&lt;/strong&gt; — Parse .docx files into clean HTML&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;html2pdf.js&lt;/strong&gt; — Render the HTML into a PDF using html2canvas + jsPDF&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Native File API&lt;/strong&gt; — File selection&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install mammoth html2pdf.js
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;loading-the-word-document&quot;&gt;Loading the Word Document&lt;/h3&gt;
&lt;p&gt;The first step is reading the uploaded .docx file into an &lt;code&gt;ArrayBuffer&lt;/code&gt;, then converting it to HTML with &lt;code&gt;mammoth&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import mammoth from 'mammoth';

async function convertDocxToHtml(file) {
  const arrayBuffer = await file.arrayBuffer();
  const result = await mammoth.convertToHtml({ arrayBuffer });
  return result.value;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;mammoth&lt;/code&gt; intentionally produces simple, clean HTML. It ignores complex formatting like text boxes and embedded fonts, which makes the output predictable.&lt;/p&gt;
&lt;p&gt;I keep the HTML in a reactive ref and render it in a preview panel:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;template&amp;gt;
  &amp;lt;div ref=&quot;previewRef&quot; class=&quot;word-preview&quot; v-html=&quot;htmlContent&quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup&amp;gt;
import { ref } from 'vue';

const htmlContent = ref('');
const previewRef = ref(null);
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;generating-the-pdf&quot;&gt;Generating the PDF&lt;/h3&gt;
&lt;p&gt;Once the user is happy with the preview, &lt;code&gt;html2pdf.js&lt;/code&gt; turns the preview element into a PDF. It uses &lt;code&gt;html2canvas&lt;/code&gt; to rasterize the HTML and &lt;code&gt;jsPDF&lt;/code&gt; to create the PDF file.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;import html2pdf from 'html2pdf.js';

async function convertToPdf() {
  const element = previewRef.value;
  if (!element) return;

  await html2pdf()
    .from(element)
    .set({
      margin: [10, 10, 10, 10],
      filename: 'document.pdf',
      image: { type: 'jpeg', quality: 0.98 },
      html2canvas: { scale: 2, useCORS: true },
      jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' },
    })
    .save();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;scale: 2&lt;/code&gt; setting gives crisp text and images on high-DPI screens. &lt;code&gt;useCORS: true&lt;/code&gt; helps if the preview contains external images.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;styling-the-preview&quot;&gt;Styling the Preview&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;html2pdf.js&lt;/code&gt; renders whatever is in the DOM, so the preview styles directly affect the output. I use scoped CSS with &lt;code&gt;:deep()&lt;/code&gt; to style the converted HTML:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.word-preview :deep(h1) {
  font-size: 1.5rem;
  font-weight: 700;
  margin-bottom: 1rem;
}

.word-preview :deep(p) {
  margin-bottom: 0.75rem;
  line-height: 1.7;
}

.word-preview :deep(table) {
  width: 100%;
  border-collapse: collapse;
  margin-bottom: 1rem;
}

.word-preview :deep(td),
.word-preview :deep(th) {
  border: 1px solid #d1d5db;
  padding: 0.5rem;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Keep the preview background white. Dark mode UI is nice for the app, but a white page produces a clean PDF.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;handling-errors-gracefully&quot;&gt;Handling Errors Gracefully&lt;/h3&gt;
&lt;p&gt;Not every file is a valid .docx. I wrap the conversion in a try/catch and reset the state on failure:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;async function handleFile(file) {
  try {
    const arrayBuffer = await file.arrayBuffer();
    const result = await mammoth.convertToHtml({ arrayBuffer });
    htmlContent.value = result.value;
  } catch (e) {
    console.error(e);
    alert('Failed to parse the document. Please check the file format.');
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also surface &lt;code&gt;mammoth&lt;/code&gt; warnings if you want to show the user which styles or elements were ignored.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;lessons-learned&quot;&gt;Lessons Learned&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. Mammoth is opinionated by design&lt;/strong&gt;&lt;br&gt;
It strips complex Word features to produce clean HTML. If your users need pixel-perfect fidelity, a server-side converter is still a better fit.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Page size matters&lt;/strong&gt;&lt;br&gt;
If the document contains wide tables or landscape sections, A4 portrait may clip them. Consider letting users choose the output format.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. html2canvas is heavy on large documents&lt;/strong&gt;&lt;br&gt;
Very long documents with many images can take several seconds and block the main thread. For heavy usage, consider chunking or using a Web Worker.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Keep the original file around&lt;/strong&gt;&lt;br&gt;
If users want to adjust settings and re-convert, you will need the original &lt;code&gt;File&lt;/code&gt; object or &lt;code&gt;ArrayBuffer&lt;/code&gt; because &lt;code&gt;mammoth&lt;/code&gt; does not expose an editable model.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;try-it-2&quot;&gt;Try It&lt;/h3&gt;
&lt;p&gt;The Word to PDF tool is live at &lt;a rel=&quot;nofollow&quot; href=&quot;https://en.sotool.top/word-to-pdf&quot;&gt;en.sotool.top/word-to-pdf&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Free, no signup, and nothing uploads to a server.&lt;/p&gt;
&lt;p&gt;Full source code is on &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/sunshey/pdf-tool&quot;&gt;GitHub&lt;/a&gt;. The conversion logic lives in &lt;code&gt;src/views/WordToPdf.vue&lt;/code&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;want-more-advanced-pdf-features-&quot;&gt;Want More Advanced PDF Features?&lt;/h3&gt;
&lt;p&gt;If you need OCR, form editing, digital signatures, or batch processing beyond what a browser tool can do, &lt;a href=&quot;https://www.anrdoezrs.net/click-101775418-14071692?url=https%3A%2F%2Fpdf.wondershare.com%2F%3Futm_source%3Dcommission-junction%26utm_medium%3Daffiliate%26utm_campaign%3D3months_epc_high_low%26utm_content%3Dlink_25136016_2025-06-11&quot; target=&quot;_blank&quot; rel=&quot;noopener sponsored nofollow&quot;&gt;Wondershare PDFelement&lt;/a&gt; is a solid desktop option. It keeps everything local and adds professional tools on top.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;This post contains affiliate links.&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Have you built client-side document converters? What did you use — mammoth, pdf-lib, or a server-side tool?&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/20522/how-to-convert-word-to-pdf-in-the-browser-with-vue-3-mammoth-and-html2pdf-js</guid>
<pubDate>Sat, 13 Jun 2026 13:24:06 +0000</pubDate>
</item>
<item>
<title>From Modal to Full Page: How We Refactored a Vue 3 Recipe Detail View (and Made It Faster)</title>
<link>https://coderlegion.com/16538/from-modal-to-full-page-how-we-refactored-a-vue-3-recipe-detail-view-and-made-it-faster</link>
<description>&lt;h1&gt;From Modal to Full Page: How We Refactored a Vue 3 Recipe Detail View (and Made It Faster)&lt;/h1&gt;
&lt;p&gt;Demo:&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://recipe-finder.org/recipe/644488-german-rhubarb-cake-with-meringue&quot;&gt;https://recipe-finder.org/recipe/644488-german-rhubarb-cake-with-meringue&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;One of the longest-lived technical decisions in our recipe finder app was showing recipe details inside a dialog modal. It worked — until it didn't. Here's how we migrated from a bloated modal to a clean, SEO-friendly full page, what we cut along the way, and what the app looks like now.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;The Old Approach: Everything in a Modal&lt;/h2&gt;
&lt;p&gt;The original setup opened a &lt;code&gt;&amp;lt;v-dialog&amp;gt;&lt;/code&gt; when a user clicked a recipe card. The modal held the entire recipe detail UI: ingredients, nutrition, videos, AI chef, grocery import, recipe scaler — all of it. The logic for opening it, fetching the recipe, and handling deep-link slugs lived inside &lt;code&gt;HomePage.vue&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Old: HomePage.vue controlled everything --&amp;gt;
&amp;lt;RecipeDetailsModal
  :is-open=&quot;isRecipeModalOpen&quot;
  :recipe=&quot;selectedRecipeDetails&quot;
  :loading=&quot;loadingRecipeDetails&quot;
  @close=&quot;closeModal&quot;
/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;The problem was that &lt;code&gt;HomePage.vue&lt;/code&gt; had become a god component. It managed:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;The search form and results&lt;/li&gt;
  &lt;li&gt;Cuisine carousel&lt;/li&gt;
  &lt;li&gt;Recipe of the day&lt;/li&gt;
  &lt;li&gt;Recent recipes&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;And&lt;/strong&gt; the modal open/close state, slug parsing, and detail fetch&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On top of that, because everything was in a modal, the URL never changed. Users couldn't share a link to a specific recipe, Google couldn't index the content, and the Back button did nothing useful.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;The New Approach: Dedicated Route + Page&lt;/h2&gt;
&lt;p&gt;We created &lt;code&gt;/recipe/:slug&lt;/code&gt; as a proper route and moved the recipe detail logic into a standalone &lt;code&gt;RecipeDetailPage.vue&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// router/index.ts
{
  path: '/recipe/:slug',
  component: () =&amp;gt; import('@/pages/RecipeDetailPage.vue'),
  meta: { title: 'Recipe' }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Slugs are derived from the recipe ID and title, making them human-readable and stable:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// utils/index.ts
export const toRecipeSlug = (id: number, title: string): string =&amp;gt; {
  const slug = title
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/(^-|-$)/g, '');
  return `${id}-${slug}`;
};

export const extractRecipeIdFromSlug = (slug: string): number | null =&amp;gt; {
  const id = parseInt(slug.split('-')[0]);
  return isNaN(id) ? null : id;
};&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;What We Cut From HomePage.vue&lt;/h2&gt;
&lt;p&gt;Once the page was independent, we stripped &lt;code&gt;HomePage.vue&lt;/code&gt; of everything modal-related. Gone:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;code&gt;isRecipeModalOpen&lt;/code&gt; ref&lt;/li&gt;
  &lt;li&gt;&lt;code&gt;selectedRecipeDetails&lt;/code&gt; and &lt;code&gt;loadingRecipeDetails&lt;/code&gt; state&lt;/li&gt;
  &lt;li&gt;The &lt;code&gt;openRecipeModal&lt;/code&gt; / &lt;code&gt;closeModal&lt;/code&gt; handlers&lt;/li&gt;
  &lt;li&gt;The slug-watching &lt;code&gt;watch&lt;/code&gt; that re-fetched on URL change&lt;/li&gt;
  &lt;li&gt;The &lt;code&gt;&amp;lt;RecipeDetailsModal&amp;gt;&lt;/code&gt; import and component registration&lt;/li&gt;
  &lt;li&gt;The &lt;code&gt;handleOpenRecipeDetails&lt;/code&gt; function passed down through three component layers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The result was &lt;code&gt;HomePage.vue&lt;/code&gt; shrinking by roughly &lt;strong&gt;40%&lt;/strong&gt; in script size. It now does one thing: show the search form and results.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;What the Page Layout Looks Like&lt;/h2&gt;
&lt;p&gt;The page uses a standard Vuetify two-column grid — main content on the left, sticky sidebar on the right (desktop only). On mobile, the sidebar collapses and the tools surface inline.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;v-row&amp;gt;
  &amp;lt;!-- Left: main content --&amp;gt;
  &amp;lt;v-col cols=&quot;12&quot; lg=&quot;8&quot;&amp;gt;
    &amp;lt;v-img :src=&quot;recipe.image&quot; cover rounded=&quot;lg&quot; class=&quot;mb-6&quot; /&amp;gt;
    &amp;lt;h1 class=&quot;recipe-title&quot;&amp;gt;{{ recipe.title }}&amp;lt;/h1&amp;gt;
    &amp;lt;!-- chips, rating, action buttons, summary, ingredients, instructions, nutrition --&amp;gt;
  &amp;lt;/v-col&amp;gt;

  &amp;lt;!-- Right: sticky sidebar (desktop only) --&amp;gt;
  &amp;lt;v-col cols=&quot;12&quot; lg=&quot;4&quot; class=&quot;d-none d-lg-flex flex-column&quot;&amp;gt;
    &amp;lt;div class=&quot;sidebar-sticky&quot;&amp;gt;
      &amp;lt;!-- Recipe Tools card --&amp;gt;
      &amp;lt;!-- AI Cooking Chef card --&amp;gt;
      &amp;lt;!-- Nutrition Snapshot card --&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/v-col&amp;gt;
&amp;lt;/v-row&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The sidebar cards use a glass morphism style that matches the rest of the app:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.glass-card {
  border: 1px solid rgba(255, 255, 255, 0.1);
  background:
    radial-gradient(circle at top right, rgba(255, 163, 92, 0.08), transparent 40%),
    linear-gradient(165deg, rgba(255, 255, 255, 0.07), rgba(255, 255, 255, 0.02));
  border-radius: 16px !important;
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;Mobile: Icon Buttons + Bottom Sheet AI&lt;/h2&gt;
&lt;p&gt;On mobile, the page can't show a sidebar. We solved this in two ways:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Action buttons&lt;/strong&gt; become icon-only on small screens, matching how the modal used to look:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Mobile: icon-only --&amp;gt;
&amp;lt;div class=&quot;d-flex d-sm-none gap-2 mb-6 align-center&quot;&amp;gt;
  &amp;lt;v-btn icon variant=&quot;tonal&quot; size=&quot;small&quot; @click.stop=&quot;handleToggleFavoriteRecipe&quot;&amp;gt;
    &amp;lt;v-icon&amp;gt;{{ isFavorited ? 'mdi-heart' : 'mdi-heart-outline' }}&amp;lt;/v-icon&amp;gt;
  &amp;lt;/v-btn&amp;gt;
  &amp;lt;v-btn icon variant=&quot;tonal&quot; size=&quot;small&quot; @click=&quot;shareRecipe&quot;&amp;gt;
    &amp;lt;v-icon&amp;gt;{{ isCopied ? 'mdi-check' : 'mdi-share-variant' }}&amp;lt;/v-icon&amp;gt;
  &amp;lt;/v-btn&amp;gt;
  &amp;lt;v-btn icon variant=&quot;tonal&quot; size=&quot;small&quot; @click=&quot;printRecipe&quot;&amp;gt;
    &amp;lt;v-icon&amp;gt;mdi-printer&amp;lt;/v-icon&amp;gt;
  &amp;lt;/v-btn&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;!-- Desktop: text buttons --&amp;gt;
&amp;lt;div class=&quot;d-none d-sm-flex flex-wrap gap-2 mb-6&quot;&amp;gt;
  &amp;lt;v-btn prepend-icon=&quot;mdi-heart-outline&quot; variant=&quot;tonal&quot; class=&quot;page-action-btn&quot;&amp;gt;
    Add to Favorites
  &amp;lt;/v-btn&amp;gt;
  &amp;lt;!-- ... --&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;The AI chef&lt;/strong&gt; becomes a bottom sheet triggered by a text button inline with the ingredient tools (no floating action button cluttering the screen):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- Ingredient row: Scale / Analyze / Grocery / AI Chef --&amp;gt;
&amp;lt;v-btn color=&quot;primary&quot; variant=&quot;text&quot; size=&quot;small&quot;
  prepend-icon=&quot;mdi-robot-excited&quot; @click=&quot;showAiSheet = true&quot;&amp;gt;
  AI Chef
&amp;lt;/v-btn&amp;gt;

&amp;lt;v-bottom-sheet v-model=&quot;showAiSheet&quot;&amp;gt;
  &amp;lt;!-- full AI interface --&amp;gt;
&amp;lt;/v-bottom-sheet&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;SEO + Meta Tags&lt;/h2&gt;
&lt;p&gt;Since the content is now on a real URL, we inject &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; and Open Graph meta tags dynamically on load:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const injectMetaTags = (title: string, summary: string, imageUrl: string) =&amp;gt; {
  const pageTitle = `${title} | Recipe Finder`;
  document.title = pageTitle;

  const setMeta = (selector: string, attr: string, value: string) =&amp;gt; {
    document.querySelector(selector)?.setAttribute(attr, value);
  };

  setMeta(&quot;meta[property='og:title']&quot;, 'content', pageTitle);
  setMeta(&quot;meta[name='twitter:title']&quot;, 'content', pageTitle);

  if (summary) {
    const clean = summary.replace(/&amp;lt;[^&amp;gt;]*&amp;gt;/g, '').trim().slice(0, 155);
    setMeta(&quot;meta[name='description']&quot;, 'content', clean);
    setMeta(&quot;meta[property='og:description']&quot;, 'content', clean);
  }

  if (imageUrl) {
    setMeta(&quot;meta[property='og:image']&quot;, 'content', imageUrl);
  }
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is called inside a &lt;code&gt;watch&lt;/code&gt; on the recipe computed ref, so it fires on both initial load and when navigating between similar recipes.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Async Components Throughout&lt;/h2&gt;
&lt;p&gt;Every non-critical UI piece is lazy-loaded:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const ImportToGroceryList = defineAsyncComponent(
  () =&amp;gt; import('@/components/ImportToGroceryList.vue')
);
const PremiumUpgradeDialog = defineAsyncComponent(
  () =&amp;gt; import('@/components/PremiumUpgradeDialog.vue')
);
const RecipeScaler = defineAsyncComponent(
  () =&amp;gt; import('@/components/RecipeScaler.vue')
);
const RecipeEmbedWatermark = defineAsyncComponent(
  () =&amp;gt; import('@/components/RecipeEmbedWatermark.vue')
);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The main recipe content renders immediately. The grocery dialog, scaler, embed widget, and upgrade dialog only load if the user actually interacts with them.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Auth Guard on Grocery Import&lt;/h2&gt;
&lt;p&gt;One regression we caught: clicking &quot;Send to Grocery List&quot; while logged out was calling &lt;code&gt;getAccessTokenSilently()&lt;/code&gt; and throwing an Auth0 missing refresh token error. Fixed by checking auth state before opening the dialog:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const onDialogToggle = (open: boolean) =&amp;gt; {
  if (open) {
    if (!isAuthenticated.value) {
      dialogOpen.value = false;
      loginWithRedirect();
      return;
    }
    loadGroceryLists();
  }
};&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;The Result&lt;/h2&gt;
&lt;p&gt;HomePage.vue Script Size: Reduced from ~650 lines to ~390 lines&lt;/p&gt;
&lt;p&gt;Recipe URL: Now fully shareable&lt;/p&gt;
&lt;p&gt;Google Indexing: Now fully indexable&lt;/p&gt;
&lt;p&gt;Mobile Layout: Upgraded from a fullscreen modal to a native page with a bottom sheet&lt;/p&gt;
&lt;p&gt;Auth Crash on Grocery: Fixed&lt;/p&gt;
&lt;p&gt;AI on Mobile: Moved from a floating button to an inline trigger&lt;br&gt;
&lt;/p&gt;&lt;p&gt;The modal still exists for the recipe list view (quick-peek without leaving the page), but the canonical experience is now a proper page. The code is cleaner, the app is faster to load, and every recipe finally has a real URL.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Built with Vue 3, Vuetify 3, TypeScript, and Tailwind CSS. Auth via Auth0.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/16538/from-modal-to-full-page-how-we-refactored-a-vue-3-recipe-detail-view-and-made-it-faster</guid>
<pubDate>Sun, 03 May 2026 19:43:07 +0000</pubDate>
</item>
<item>
<title>Creative Ways to Leverage JavaScript Frameworks for Modern Development</title>
<link>https://coderlegion.com/11630/creative-ways-to-leverage-javascript-frameworks-for-modern-development</link>
<description>&lt;h4 id=&quot;introduction&quot;&gt;Introduction&lt;/h4&gt;
&lt;p&gt;As a JavaScript developer, you know that libraries and frameworks provide powerful tools for building applications. However, tapping into their full potential often requires thinking outside the box. Let's explore some innovative ways to leverage frameworks like React, Vue, and Angular.&lt;/p&gt;
&lt;h4 id=&quot;1-component-reusability&quot;&gt;1. Component Reusability&lt;/h4&gt;
&lt;p&gt;Instead of building every new feature from scratch, focus on creating highly reusable components. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; Suppose you're developing an e-commerce site. Create a generic &lt;code&gt;ProductCard&lt;/code&gt; component that can adapt to different product types by passing props, simplifying future updates and feature additions.&lt;/p&gt;
&lt;h4 id=&quot;2-custom-hooks-in-react&quot;&gt;2. Custom Hooks in React&lt;/h4&gt;
&lt;p&gt;Custom hooks can significantly streamline your code and improve maintainability. You can encapsulate common logic into hooks.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; If multiple components need to fetch user data, develop a &lt;code&gt;useFetchUser&lt;/code&gt; hook that handles fetching, loading, and error states. This keeps components clean and promotes logic reuse.&lt;/p&gt;
&lt;h4 id=&quot;3-vue-mixins-for-shared-functionality&quot;&gt;3. Vue Mixins for Shared Functionality&lt;/h4&gt;
&lt;p&gt;While Vue has a robust composition API, mixins are still a potent tool for sharing functionality across components.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; Create a mixin that includes validation methods. This can simplify form controls across various components, ensuring consistency and reducing duplication.&lt;/p&gt;
&lt;h4 id=&quot;4-angular-schematics&quot;&gt;4. Angular Schematics&lt;/h4&gt;
&lt;p&gt;Angular's schematics allow you to automate the creation of components, services, and more. This feature improves efficiency by generating boilerplate code.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; Customize schematics for your app, so when you generate a new component, it automatically includes CSS styles, tests, and related files. This drastically reduces setup time.&lt;/p&gt;
&lt;h4 id=&quot;5-state-management-through-context-api&quot;&gt;5. State Management through Context API&lt;/h4&gt;
&lt;p&gt;For small to medium apps, you might not need a heavy state management library like Redux. Instead, consider using the Context API to manage state effectively without the overhead.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; Build a simple global store for user authentication status, which can be easily accessed across components without prop drilling.&lt;/p&gt;
&lt;h4 id=&quot;6-progressive-web-apps-with-frameworks&quot;&gt;6. Progressive Web Apps with Frameworks&lt;/h4&gt;
&lt;p&gt;Both React and Vue support PWA features enabling you to create fast, reliable apps. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; Use Create React App’s built-in PWA support to enhance your web app, making it installable and improving performance metrics.&lt;/p&gt;
&lt;h4 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h4&gt;
&lt;p&gt;By adopting these unconventional techniques in your JavaScript projects, you can streamline your workflow and develop high-quality applications more efficiently. Don’t hesitate to mix and match practices from different frameworks to find what works best for your needs.&lt;/p&gt;
&lt;p&gt;Share your thoughts on these strategies or suggest other creative approaches you've implemented.&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/11630/creative-ways-to-leverage-javascript-frameworks-for-modern-development</guid>
<pubDate>Sat, 14 Feb 2026 07:33:52 +0000</pubDate>
</item>
<item>
<title>Vector: The easiest way to plug Vue in Blade</title>
<link>https://coderlegion.com/11004/vector-the-easiest-way-to-plug-vue-in-blade</link>
<description>&lt;p&gt;You know that feeling when you're building a Laravel app and you just need a tiny bit of reactivity? A counter. A toggle. Something that feels overkill for a full Vue component but too annoying for vanilla JavaScript?&lt;/p&gt;
&lt;p&gt;I kept reaching for Alpine.js, which is great, but I wanted Vue's Composition API. The &lt;code&gt;ref()&lt;/code&gt;, the &lt;code&gt;computed()&lt;/code&gt;, the familiar syntax I already know. So I bu4ilt Vector.&lt;/p&gt;
&lt;h3 id=&quot;what-even-is-this-&quot;&gt;What Even Is This?&lt;/h3&gt;
&lt;p&gt;Vector is a Laravel package that lets you write Vue directly in your Blade templates with zero ceremony:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup&amp;gt;
    const i = ref(0);
&amp;lt;/script&amp;gt;

&amp;lt;div&amp;gt;
    &amp;lt;button @click=&quot;i++&quot;&amp;gt;Click Me&amp;lt;/button&amp;gt;
    &amp;lt;div&amp;gt;
        Count: @{{ i }}
    &amp;lt;/div&amp;gt;
    &amp;lt;div v-if=&quot;i &amp;gt; 5&quot;&amp;gt;Success!&amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's the whole thing. No build step for your components. No separate &lt;code&gt;.vue&lt;/code&gt; files. No special directives wrapping your code. Just a &lt;code&gt;&amp;lt;script setup&amp;gt;&lt;/code&gt; tag and you're done.&lt;/p&gt;
&lt;h3 id=&quot;how-it-works&quot;&gt;How It Works&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;&amp;lt;script setup&amp;gt;&lt;/code&gt; tag gets transformed at compile time. Vector treats the &lt;strong&gt;element immediately after&lt;/strong&gt; the script tag as your Vue template. Everything inside that element becomes reactive, and anything outside it remains regular Blade.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Blade's precompiler finds your &lt;code&gt;&amp;lt;script setup&amp;gt;&lt;/code&gt; blocks&lt;/li&gt;
&lt;li&gt;Extracts your variable declarations&lt;/li&gt;
&lt;li&gt;Mounts Vue on the next sibling element&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The key part is the variable extraction. It parses &lt;code&gt;const&lt;/code&gt;, &lt;code&gt;let&lt;/code&gt;, and &lt;code&gt;var&lt;/code&gt; declarations and auto-returns them to the template. You write normal code, it figures out the rest.&lt;/p&gt;
&lt;h4 id=&quot;escaping-blade-syntax&quot;&gt;Escaping Blade Syntax&lt;/h4&gt;
&lt;p&gt;Since Blade also uses &lt;code&gt;{{ }}&lt;/code&gt; for output, you need to prefix Vue's mustache syntax with &lt;code&gt;@&lt;/code&gt; to prevent Blade from processing it:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;{{-- This is Blade --}}
{{ $phpVariable }}

{{-- This is Vue (note the @) --}}
@{{ vueVariable }}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Alternatively, use Vue directives like &lt;code&gt;v-text&lt;/code&gt; which don't conflict with Blade:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;span v-text=&quot;count&quot;&amp;gt;&amp;lt;/span&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;installation-1&quot;&gt;Installation&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;composer require brunoabpinto/vector
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add Vector to your Vite entry points in &lt;code&gt;vite.config.js&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;plugins: [
    laravel({
        input: [
            &quot;resources/css/app.css&quot;,
            &quot;resources/js/app.js&quot;,
            &quot;resources/js/vendor/vector.js&quot;,
        ],
        // ...
    }),
],
resolve: {
    alias: {
        'vue': 'vue/dist/vue.esm-bundler.js',
    },
},
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add &lt;code&gt;@vectorJs&lt;/code&gt; before your closing &lt;code&gt;&amp;lt;/body&amp;gt;&lt;/code&gt; tag in your layout:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-php&quot;&gt;&amp;lt;body&amp;gt;
    {{ $slot }}

    @vectorJs
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's it. Vector auto-publishes its runtime, and &lt;code&gt;@vectorJs&lt;/code&gt; loads it where you need it.&lt;/p&gt;
&lt;h3 id=&quot;the-trade-offs&quot;&gt;The Trade-offs&lt;/h3&gt;
&lt;p&gt;Let's be real about what this is:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Good for:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Quick interactive elements&lt;/li&gt;
&lt;li&gt;Prototyping&lt;/li&gt;
&lt;li&gt;When you want Vue's API without Vue's ceremony&lt;/li&gt;
&lt;li&gt;Laravel apps that are mostly server-rendered with islands of reactivity&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Not great for:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Complex component hierarchies&lt;/li&gt;
&lt;li&gt;When you need proper SFC features (scoped styles, etc.)&lt;/li&gt;
&lt;li&gt;Large-scale SPAs (just use Inertia at that point)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;try-it-3&quot;&gt;Try It&lt;/h3&gt;
&lt;p&gt;The package is available on &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/brunoabpinto/vector&quot;&gt;GitHub&lt;/a&gt;. Star it, fork it, tell me it's an abomination. Whatever feels right.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;composer require brunoabpinto/vector
&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/11004/vector-the-easiest-way-to-plug-vue-in-blade</guid>
<pubDate>Tue, 03 Feb 2026 19:24:34 +0000</pubDate>
</item>
<item>
<title>Beyond the Framework Hype: What Really Matters in Modern Web Dev</title>
<link>https://coderlegion.com/7974/beyond-the-framework-hype-what-really-matters-in-modern-web-dev</link>
<description>&lt;p&gt;Modern web development in 2025 is dominated by frameworks, but the real story is not &lt;strong&gt;React vs Vue vs Svelte&lt;/strong&gt;, it is how &lt;strong&gt;well&lt;/strong&gt; teams use these tools to &lt;strong&gt;ship fast, accessible, maintainable products&lt;/strong&gt;. React remains the default choice for many companies because of its vast ecosystem, strong TypeScript support, and mature meta‑frameworks like Next.js and Remix, which handle routing, data fetching, and server‑side rendering out of the box. At the same time, lighter options such as Preact, Solid, and Svelte are gaining traction where performance and bundle size are critical, especially on mobile connections or embedded environments.&lt;/p&gt;
&lt;p&gt;The biggest shift is architectural rather than syntactic: &lt;em&gt;server components&lt;/em&gt;, &lt;em&gt;streaming rendering&lt;/em&gt;, and &lt;em&gt;edge functions&lt;/em&gt; are blurring the line between frontend and backend. Instead of shipping a monolithic single‑page app, developers increasingly compose UIs from server‑rendered shells with islands of interactivity, hydrated only where needed. &lt;em&gt;React Server Components, Next.js App Router&lt;/em&gt;, and &lt;em&gt;Qwik’s resumability&lt;/em&gt; are all attempts to solve the same problem—how to keep the developer experience of components while delivering near‑instant first paint for users. This mindset forces teams to think about data loading, caching, and latency as first‑class concerns rather than afterthoughts.&lt;/p&gt;
&lt;p&gt;Another quiet revolution is happening around DX: tools like Vite, Turbopack, and esbuild make dev servers nearly instant, while component‑driven workflows with Storybook and design systems keep UI consistent across large teams. Yet the best results still come from fundamentals: semantic HTML, clean state management, predictable APIs, and performance budgets baked into CI. The question is no longer &lt;strong&gt;&lt;em&gt;“Which framework should I learn first?&lt;/em&gt;&lt;/strong&gt;” but “&lt;strong&gt;How do I design my app so that swapping frameworks in five years does not become a full rewrite?&lt;/strong&gt;” In that world, good architecture.......clear boundaries, reusable domain logic, and API‑first thinking.....outlives any single JavaScript trend.&lt;/p&gt;
</description>
<category>Blockchain</category>
<guid isPermaLink="true">https://coderlegion.com/7974/beyond-the-framework-hype-what-really-matters-in-modern-web-dev</guid>
<pubDate>Sat, 29 Nov 2025 19:01:43 +0000</pubDate>
</item>
<item>
<title>How I Started Writing Unit Tests for Vue Components - Part 2</title>
<link>https://coderlegion.com/7596/how-i-started-writing-unit-tests-for-vue-components-part-2</link>
<description>&lt;p&gt;So, it's been a year since the last &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.byteminds.co.uk/blog/how-i-started-writing-unit-tests-for-vue-components&quot;&gt;article&lt;/a&gt;, and a lot has changed. In this one, we're going to talk about integrating with Mock Service Worker (MSW). I'll also describe what I tried to implement in my quest for system resilience - what worked out and what didn't.&lt;/p&gt;
&lt;h3 id=&quot;so-did-my-tests-actually-help-me-&quot;&gt;So, did my tests actually help me?&lt;/h3&gt;
&lt;p&gt;I can't say the time investment paid off in spades, but one thing's for sure - it definitely wasn't a waste of time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Here are the main areas where the tests really proved their worth:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When contracts were lost or changed;&lt;/li&gt;
&lt;li&gt;Fixing the fallout from merge conflicts (given the quirks of our&lt;br&gt;
processes, this is the most common scenario);&lt;/li&gt;
&lt;li&gt;Refactoring (it's hard to be objective here since our project's test&lt;br&gt;
coverage isn't huge, but before any refactoring, I try to at least&lt;br&gt;
cover the code with local tests).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then again, all those fancy things like Cursor with their powerful autocomplete freed up some time, so why not spend a bit of it on tests?&lt;/p&gt;
&lt;h3 id=&quot;the-fight-for-reliability-or-reality-strikes-back&quot;&gt;The Fight for Reliability, or Reality Strikes Back&lt;/h3&gt;
&lt;p&gt;Let me start with what didn't work out.The first thing I tried was implementing E2E tests with Playwright. You know, testing business logic in the browser by simulating real user actions.&lt;/p&gt;
&lt;p&gt;In our existing project, this turned out to be really tough. The main problem was setting up the initial test data. In my case, that meant the database. It needed to be as small as possible, but still have all the necessary data for testing.&lt;/p&gt;
&lt;p&gt;In theory, it sounds simple: take a database, tweak the data, create a Docker image, and boom - you're golden. Well, I got stuck at the very first step of preparing that database. It requires a firm decision and a coordinated effort, meaning help from the backend team and DevOps (who are always busy). In the end, on my project, we shelved the idea for the time being.&lt;/p&gt;
&lt;p&gt;I also tried replacing actual backend interaction by mocking API requests directly within Playwright, but that felt like a dead end. Maintaining yet another set of mocks (on top of the MSW we already had) combined with the slow browser startup times just didn't seem rational, unless for some very specific tasks.&lt;/p&gt;
&lt;h3 id=&quot;about-unit-tests-and-msw&quot;&gt;About Unit Tests and MSW&lt;/h3&gt;
&lt;p&gt;All in all, I decided to focus on unit tests (which, in the classic sense, are more like integration tests in our context). They're fast, isolated, simple, and reliable.&lt;/p&gt;
&lt;p&gt;To mock network interactions, I set up MSW (Mock Service Worker). This later allowed us to practice contract-first programming and parallel development.&lt;/p&gt;
&lt;p&gt;So, first you install MSW (the &lt;a rel=&quot;nofollow&quot; href=&quot;https://mswjs.io/docs/quick-start&quot;&gt;official guide&lt;/a&gt; is your best friend here).&lt;/p&gt;
&lt;p&gt;Then, I moved the vite configuration for it into vitest.workspace.js (note: in new versions &lt;a rel=&quot;nofollow&quot; href=&quot;https://vitest.dev/config/#projects&quot;&gt;DEPRECATED&lt;/a&gt;). This isn't mandatory, but it's convenient if you need to separate node and browser environments.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { defineWorkspace } from 'vitest/config';

export default defineWorkspace([
  'packages/*',
  {
    extends: './vite.config.js',
    test: {
      environment: 'jsdom',
      name: 'unit',
      include: ['src/**/*.spec.{ts,js}'],
      deps: {
        inline: ['element-plus'],
      },
      setupFiles: ['./src/mocks/setup.ts'], // path for the msw config
    },
  },
]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since it's an independent service, I put it in a mocks folder, so it's easy to cleanly remove if needed.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=1173094428015996746&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { server } from './server.ts';
import { afterAll, afterEach, beforeAll } from 'vitest';

beforeAll(() =&amp;gt;  return server.listen({ onUnhandledRequest: 'warn' });
afterAll(() =&amp;gt; server.close());
afterEach(() =&amp;gt; server.resetHandlers());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;user/handlers.ts&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { GET_USERS } from '@/api/constants/APIEndPoints.js';
import { HttpResponse, http } from 'msw';
import { USERS_FAKE_RESPONSE} from './fixtures.ts';

export const handlers = [
  http.get('*' + GET_USERS , () =&amp;gt; {
    return HttpResponse.json(USERS_FAKE_RESPONSE);
  }),

];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, whenever a call is made to the URL defined in the &lt;strong&gt;GET_USER&lt;/strong&gt; identifier, it will return the value stored in &lt;strong&gt;USER_FAKE_RESPONSE&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Interestingly, MSW, especially with its &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.npmjs.com/package/openapi-msw&quot;&gt;plugins&lt;/a&gt;, can generate handlers from an &lt;strong&gt;openApi.json&lt;/strong&gt; file, which can cover all your API requests. It can also use &lt;strong&gt;faker.js&lt;/strong&gt; to generate response data with fake values.&lt;/p&gt;
&lt;p&gt;I'm not a big fan of that approach myself (it can complicate parallel work), so I prefer to create response fixtures and handlers manually, and then fill them in - even using AI helpers sometimes - which results in more human-readable responses.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const USER_FAKE_RESPONSE = {
  items:[
    { firstName: 'John' , lastName: 'Smith'}
    { firstName: 'Willy' , lastName: 'Willson'}
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;using-it-in-tests&quot;&gt;Using it in Tests&lt;/h3&gt;
&lt;p&gt;For a clear example, let's imagine we have a component with a button to fetch users and a block to display the response. A traditional test might look something like this (a detailed test was in the previous article; this is just a schematic).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import * as USER_API from 'some api folder'
let wrapper

const createComponent = (params {}) =&amp;gt; {
  wrapper = shallowMount(OurGetUsersComponent, {
    props: {
      ...params.props,
    },
    global: {
      renderStubDefaultSlot: true,
      stubs: {
        ...params.stubs,
      },
    },
  });
};

test('Handling user retrieval when the Find button is clicked', async () =&amp;gt; {
const spyGetUsers =  vi.spyOn(USER_API, 'getUsersRequest').mockImplementation(() =&amp;gt;{  items:[
    { firstName: 'John' , lastName: 'Smith'}
    { firstName: 'Willy' , lastName: 'Willson'}
  ]}) 

  createComponent ()
  const buttonNode = wrapper.find('.button') //not a very good selector, but we only have 1 button
  await buttonNode.trigger('click');

  await flushPromises();
  expect(spyGetUsers).toHaveBeenCalled(); //here you can also check the parameters


  expect(wrapper.text()).toContain('Smith')
  expect(wrapper.text()).toContain('Willson')
 
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That approach works, but what if we need to test the behavior when the server returns an error? For example, when a 500 error triggers a toast notification saying, &quot;The server is temporarily unavailable, please try again later.&quot;&lt;/p&gt;
&lt;p&gt;This is exactly where MSW comes to the rescue.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { server } from '@/mocks/server';
import { http, HttpResponse } from 'msw';
import { USER_FAKE_RESPONSE } from '...fixtures'
import * as MESSAGE_MODULE from &quot;utils&quot;
import { GET_USERS } from '@/api/constants/APIEndPoints.js';

let wrapper

const createComponent = (params {}) =&amp;gt; {
  wrapper = shallowMount(OurGetUsersComponent, {
    props: {
      ...params.props,
    },
    global: {
      renderStubDefaultSlot: true,
      stubs: {
        ...params.stubs,
      },
    },
  });
};

test('Handling user retrieval when the search button is clicked', async () =&amp;gt; {
const spyGetUsers =  vi.spyOn(USER_API, 'getUsersRequest') // the implementation already exists in MSW and doesn't need to be duplicated here

  createComponent ()
  // it's better to search the same way as the user  
  const buttonNode = wrapper.findAll('.button').filter(item=&amp;gt;item.text()==&quot;Search&quot;)[0] 
  await buttonNode.trigger('click');

  await flushPromises();
  expect(spyGetUsers).toHaveBeenCalled(); // This step might be redundant since the result is what matters to the user

  expect(wrapper.text()).toContain(USER_FAKE_RESPONSE.items[0].lastName)
  expect(wrapper.text()).toContain(USER_FAKE_RESPONSE.items[1].lastName)
 
});

test('Handling server errors when retrieving users', async ()=&amp;gt;{
spyMessage = vi.spyOn(MESSAGE_MODULE , 'showErrorMessage')

  server.use(
    http.get('*' + GET_USERS, () =&amp;gt; {
      return new HttpResponse(null, { status: 500 });
    }),
  );
  createComponent ()
  const buttonNode = wrapper.findAll('.button').filter(item=&amp;gt;item.text()==&quot;Search&quot;)[0] 
  await buttonNode.trigger('click');

expect(spyMessage ).toHaveBeenCalledWith({message: 'The server is temporarily unavailable, please try again later' });

})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This way, you can make your unit tests a little more honest and your team's capabilities a little broader.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Author: Dmitry Simonov&lt;/em&gt;&lt;/p&gt;
</description>
<category>Web Dev</category>
<guid isPermaLink="true">https://coderlegion.com/7596/how-i-started-writing-unit-tests-for-vue-components-part-2</guid>
<pubDate>Fri, 21 Nov 2025 09:41:46 +0000</pubDate>
</item>
<item>
<title>Why did I create vue-uform</title>
<link>https://coderlegion.com/4887/why-did-i-create-vue-uform</link>
<description>&lt;p&gt;In Vue, we already have the very convenient v-model syntax sugar. It handles two essential things for us:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When a JavaScript variable has a value, it fills the input field.&lt;/li&gt;
&lt;li&gt;When the input changes, the new value is passed back to JavaScript.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That’s basically what v-model does under the hood.&lt;/p&gt;
&lt;p&gt;So why redefine two-way binding?&lt;/p&gt;
&lt;p&gt;The reason is that I’ve been exploring a new approach to form validation. I believe the next generation of Vue form validators should be:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Component-driven – the form behavior is powered by components, not configuration.&lt;/li&gt;
&lt;li&gt;Style-agnostic – no built-in styling, leaving developers free to define layouts.&lt;/li&gt;
&lt;li&gt;Flexible with UI libraries – should work seamlessly with input components from libraries like Naive UI, Element Plus, or even fully custom ones.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What existing validators do&lt;/p&gt;
&lt;p&gt;When I looked at existing solutions, I found a common pattern: each form field usually comes with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A label&lt;/li&gt;
&lt;li&gt;An input&lt;/li&gt;
&lt;li&gt;A help message&lt;/li&gt;
&lt;li&gt;An error message (shown when validation fails)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Some libraries solve this by bundling their own input components and tightly integrating them with validation. While this works, it reduces flexibility.&lt;/p&gt;
&lt;p&gt;I wanted something different: a validator that lets developers use their own UI components, while still handling value binding and validation logic cleanly.&lt;/p&gt;
&lt;p&gt;The idea: u-field component&lt;/p&gt;
&lt;p&gt;To achieve this, I introduced the idea of a u-field component.&lt;br&gt;
You can put any input component into its slot.&lt;/p&gt;
&lt;p&gt;The u-field exposes two key bindings to its slot:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;value&lt;/strong&gt; passes the form data down to the input.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;update&lt;/strong&gt; sends the new input value back up to the form.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This makes synchronization possible, and sets the stage for validation.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;template&amp;gt;
  &amp;lt;u-field v-slot=&quot;{ value, update }&quot;&amp;gt;
    &amp;lt;input :value=&quot;value&quot; @input=&quot;($event) =&amp;gt; update($event.target.value)&quot; /&amp;gt;
  &amp;lt;/u-field&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why not just v-model?&lt;/p&gt;
&lt;p&gt;Looking at Vue’s official docs, this is exactly the kind of code v-model was meant to simplify.&lt;br&gt;
But when binding with parent components (not just local state), we need another kind of syntax sugar.&lt;/p&gt;
&lt;p&gt;So, inspired by vue-macros, I created a Vite plugin to introduce a new directive: &lt;code&gt;f-model&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;With &lt;code&gt;f-model&lt;/code&gt;, the example above becomes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;u-field v-slot=&quot;{ value, update }&quot;&amp;gt;
  &amp;lt;input f-model /&amp;gt;
&amp;lt;/u-field&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Beyond text inputs&lt;/p&gt;
&lt;p&gt;Of course, text inputs are the simplest case.&lt;br&gt;
For other components like radio buttons, checkboxes, or selects, special handling is still needed.&lt;/p&gt;
&lt;p&gt;For third-party components, it’s straightforward:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;With a regular input:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;input f-model /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;With Naive UI’s n-input (which uses v-model:value):&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;n-input f-model:value /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/4887/why-did-i-create-vue-uform</guid>
<pubDate>Thu, 21 Aug 2025 02:07:44 +0000</pubDate>
</item>
<item>
<title>I built vue-uform – a unstyled, component-driven form validation library for Vue 3</title>
<link>https://coderlegion.com/4803/i-built-vue-uform-a-unstyled-component-driven-form-validation-library-for-vue-3</link>
<description>&lt;h2 id=&quot;hey-everyone&quot;&gt;Hey everyone&lt;/h2&gt;
&lt;p&gt;I’ve been working on a Vue 3 form library called &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/tu6ge/vue-uform&quot;&gt;vue-uform&lt;/a&gt;, and I think it might be useful for folks who want &lt;em&gt;flexible&lt;/em&gt;, &lt;em&gt;unstyled&lt;/em&gt;, &lt;em&gt;component-driven&lt;/em&gt; form validation without the boilerplate.&lt;/p&gt;
&lt;h2 id=&quot;why-i-built-it&quot;&gt;Why I built it&lt;/h2&gt;
&lt;p&gt;Most form libraries either:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Come with a bunch of built-in styles that don’t match your design system, or&lt;/li&gt;
&lt;li&gt;Require too much JS/TS setup for simple forms.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;vue-uform&lt;/em&gt; takes a different approach:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No built-in styles – you have full control over HTML &amp;amp; CSS.&lt;/li&gt;
&lt;li&gt;Treats components as first-class citizens – validation, data flow, and UI are all component-based.&lt;/li&gt;
&lt;li&gt;Works with any UI library (Element Plus, Naive UI, etc.).&lt;/li&gt;
&lt;li&gt;Supports built-in &amp;amp; custom validation rules (inspired by FormKit).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;quick-example&quot;&gt;Quick Example&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;script setup&amp;gt;
const formValues = { username: '', password: '' }

function doLogin(data) {
  console.log(data)
}
&amp;lt;/script&amp;gt;
&amp;lt;template&amp;gt;
  &amp;lt;u-form :values=&quot;formValues&quot; @submit=&quot;doLogin&quot;&amp;gt;
    &amp;lt;u-field name=&quot;username&quot; label=&quot;Username&quot; validation=&quot;required&quot; v-slot=&quot;{ value, update }&quot;&amp;gt;
      &amp;lt;input f-model /&amp;gt;
    &amp;lt;/u-field&amp;gt;

    &amp;lt;u-field name=&quot;password&quot; label=&quot;Password&quot; validation=&quot;required|min:6&quot; v-slot=&quot;{ value, update }&quot;&amp;gt;
      &amp;lt;input type=&quot;password&quot; f-model /&amp;gt;
    &amp;lt;/u-field&amp;gt;

    &amp;lt;u-submit&amp;gt;Login&amp;lt;/u-submit&amp;gt;
  &amp;lt;/u-form&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;That f-model directive is provided by a small Vite plugin – it’s like v-model but tailored for form fields.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;-built-in-validation-rules-include-&quot;&gt;✅ Built-in validation rules include:&lt;/h2&gt;
&lt;p&gt;required, number, email, between, starts_with, url, and more – you can also add your own custom rules with parameters&lt;/p&gt;
&lt;p&gt;Example custom rule:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
function isfruit(node: FieldNode): boolean | string {
  const { value } = node;
  if (value.value != &quot;apple&quot; &amp;amp;&amp;amp; value.value != &quot;banana&quot;) {
    return &quot;this value is not apple or banana&quot;;
  }
  return true;
}
&amp;lt;/script&amp;gt;
&amp;lt;template&amp;gt;
    &amp;lt;u-field
      name=&quot;fruit&quot;
      label=&quot;Fruit&quot;
      help=&quot;please input a fruit&quot;
      :rules=&quot;{ isfruit }&quot;
      validation=&quot;required|isfruit&quot;
      v-slot=&quot;{ value, update }&quot;
    &amp;gt;
      &amp;lt;input f-model /&amp;gt;
    &amp;lt;/u-field&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;installation-2&quot;&gt;Installation&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pnpm install vue-uform
pnpm install @vue-uform/vite-plugin -D
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;links&quot;&gt;Links&lt;/h2&gt;
&lt;p&gt;GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/tu6ge/vue-uform&quot;&gt;https://github.com/tu6ge/vue-uform&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;StackBlitz: &lt;a rel=&quot;nofollow&quot; href=&quot;https://stackblitz.com/~/github.com/tu6ge/vue-uform-example&quot;&gt;https://stackblitz.com/~/github.com/tu6ge/vue-uform-example&lt;/a&gt;&lt;/p&gt;
</description>
<category>vue.js</category>
<guid isPermaLink="true">https://coderlegion.com/4803/i-built-vue-uform-a-unstyled-component-driven-form-validation-library-for-vue-3</guid>
<pubDate>Mon, 18 Aug 2025 20:25:28 +0000</pubDate>
</item>
<item>
<title># Automated Anomaly Detection and Predictive Maintenance in Drone Motor Control Systems</title>
<link>https://coderlegion.com/4499/automated-anomaly-detection-and-predictive-maintenance-in-drone-motor-control-systems</link>
<description>&lt;p&gt;Automated Anomaly Detection and Predictive Maintenance in Drone Motor Control Systems using Federated Learning and Gaussian Process Regression&lt;/p&gt;
&lt;p&gt;Abstract: This research proposes a robust and scalable solution for anomaly detection and predictive maintenance in drone motor control systems utilizing a federated learning framework combined with Gaussian Process Regression (GPR). Traditional centralized approaches to anomaly detection suffer from data privacy concerns and struggle to adapt to the diverse operating conditions of individual drones. Federated learning allows for model training across decentralized datasets residing on the drones themselves, preserving data privacy while leveraging a global model. Integration with GPR provides uncertainty quantification and enables accurate prediction of motor degradation, facilitating proactive maintenance and minimizing downtime. This system is industrially viable, offering immediate improvements in drone fleet management with an estimated 15-20% reduction in maintenance costs and a 5-10% improvement in operational reliability.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Introduction&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The drone industry continues to experience exponential growth across various sectors, including logistics, agriculture, surveillance, and infrastructure inspection. A critical component of drone operation is the motor control system, responsible for efficient and precise flight control. Unexpected motor failures pose significant risks, leading to operational downtime, potential damage, and safety hazards. Current anomaly detection methods often rely on centralized data collection, which raises data privacy concerns and hinders adaptability to varied flight conditions. This research addresses these limitations by introducing a federated learning and Gaussian Process Regression (GPR) based approach that distributes model training while providing accurate predictive maintenance capabilities.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Related Work &amp;amp; Novelty&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Existing drone anomaly detection methods commonly employ machine learning algorithms like Support Vector Machines (SVMs) or Recurrent Neural Networks (RNNs), trained on centralized datasets. These methods, while effective, are vulnerable to single points of failure and privacy breaches. Federated learning has emerged as a promising alternative for distributed machine learning, protecting data privacy while aggregating knowledge. However, few existing applications integrate federated learning with accurate predictive models capable of quantifying uncertainty. This study uniquely combines federated learning with Gaussian Process Regression (GPR) to achieve both privacy preservation and robust predictive maintenance, exceeding the performance of traditional centralized methods and providing greater operational insight. The novel aspect lies in the adaptive weighting of each drone’s contribution during the federated learning process, dynamically adjusted based on the quality and variance of their local data.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Methodology&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;3.1 System Architecture: The system comprises three key components: the Drone Agent, the Federated Learning Server (FLS), and the Maintenance Management Interface (MMI).&lt;/p&gt;
&lt;p&gt;Drone Agent: Each drone acts as a client in the federated learning process. It collects real-time data from motor control sensors (current, voltage, temperature, RPM, vibration frequency) and trains a local GPR model on this data.&lt;/p&gt;
&lt;p&gt;Federated Learning Server (FLS): The FLS orchestrates the federated learning process. It distributes shared GPR model architectures to each drone agent and aggregates their locally trained model parameters using a secure aggregation protocol.&lt;/p&gt;
&lt;p&gt;Maintenance Management Interface (MMI): The MMI presents a dashboard for visualizing model performance, anomaly scores, and predicted remaining useful life (RUL) of each motor.&lt;/p&gt;
&lt;p&gt;3.2 Federated Learning Algorithm: The federated learning process follows the FedAvg algorithm, modified to incorporate adaptive weighting.&lt;/p&gt;
&lt;p&gt;Initialization: Each drone receives an initialized GPR model with randomly generated hyperparameters.&lt;/p&gt;
&lt;p&gt;Local Training: Each Drone Agent trains its local GPR model using data collected over a defined time window (T) using stochastic gradient descent.&lt;/p&gt;
&lt;p&gt;Model Aggregation: The FLS aggregates the locally trained model parameters (weights, hyperparameters) from each drone using a weighted averaging scheme:&lt;/p&gt;
&lt;p&gt;global_model_parameters = SUM (from i=1 to N) of (w_i * local_model_parameters_i)&lt;/p&gt;
&lt;p&gt;Where:&lt;/p&gt;
&lt;p&gt;global_model_parameters is the global GPR model parameters (denoted as theta_global).&lt;/p&gt;
&lt;p&gt;N is the number of participating drones.&lt;/p&gt;
&lt;p&gt;w_i is the weight assigned to the i-th drone.&lt;/p&gt;
&lt;p&gt;local_model_parameters_i is the set of parameters from the i-th drone’s local GPR model (denoted as theta_i).&lt;/p&gt;
&lt;p&gt;The weights (w_i) are dynamically adjusted based on a variance metric of each drone’s local training data:&lt;/p&gt;
&lt;p&gt;w_i = 1 / (local_data_variance_i)&lt;/p&gt;
&lt;p&gt;Where:&lt;/p&gt;
&lt;p&gt;local_data_variance_i is the variance of the local training data for drone i (denoted as sigma_i^2).&lt;/p&gt;
&lt;p&gt;Iteration: The process repeats for a fixed number of iterations.&lt;/p&gt;
&lt;p&gt;3.3 Gaussian Process Regression (GPR): GPR is employed for anomaly detection and RUL prediction due to its ability to provide probabilistic predictions and quantify uncertainty.&lt;/p&gt;
&lt;p&gt;Kernel Selection: The Matérn kernel is chosen due to its flexibility in capturing varying degrees of smoothness in the motor degradation process.&lt;/p&gt;
&lt;p&gt;RUL Prediction: The RUL of each motor is predicted by fitting the GPR model to historical data and extrapolating the degradation trend.&lt;/p&gt;
&lt;p&gt;Anomaly Detection: Anomalies are flagged when the observed sensor readings deviate significantly from the GPR’s predicted value, exceeding a pre-defined threshold based on the GPR’s predictive variance.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Experimental Design&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;4.1 Dataset: A simulated dataset of drone motor control data is generated using a physics-based motor model, incorporating various degradation mechanisms (wear, friction, corrosion). The dataset consists of 100 drones, each operating under diverse environmental conditions. The dataset contains 1 million data points per drone.&lt;/p&gt;
&lt;p&gt;4.2 Baseline Comparison: The proposed federated learning + GPR system will be compared against the following baseline methods:&lt;/p&gt;
&lt;p&gt;Centralized GPR: A standard GPR model trained on a centralized dataset of all drone data.&lt;/p&gt;
&lt;p&gt;Independent GPR: Each drone trains its own GPR model independently without federated learning.&lt;/p&gt;
&lt;p&gt;4.3 Evaluation Metrics: The following metrics will be used to evaluate system performance:&lt;/p&gt;
&lt;p&gt;Anomaly Detection Accuracy: Precision, Recall, F1-score&lt;/p&gt;
&lt;p&gt;RUL Prediction Error: Root Mean Squared Error (RMSE)&lt;/p&gt;
&lt;p&gt;Communication Overhead: Average communication rounds required for convergence.&lt;/p&gt;
&lt;p&gt;Privacy Preservation: Differential Privacy parameter (epsilon, delta). A higher value indicates stronger privacy guarantees.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Data Analysis and Results Prediction&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We predict that the federated learning + GPR system will outperform both baseline methods in terms of anomaly detection accuracy and RUL prediction error while preserving data privacy. The adaptive weighting scheme is expected to improve convergence speed and overall model accuracy. The system is expected to achieve an F1-score of 0.95 for anomaly detection, an RMSE of 5% for RUL prediction, and a differential privacy parameter of (epsilon, delta) = (0.1, 1e-6). A communication overhead of roughly 15-20 rounds is expected for convergence.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Scalability Roadmap&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Short-Term (6-12 months): Deploy the system on a fleet of 100 drones, validating performance in a real-world environment and refining model parameters. Optimize communication protocols for low-bandwidth environments.&lt;/p&gt;
&lt;p&gt;Mid-Term (1-3 years): Scale the system to a fleet of 1000 drones, incorporating support for heterogeneous drone models and sensor configurations. Investigate using blockchain technology for secure and auditable data aggregation.&lt;/p&gt;
&lt;p&gt;Long-Term (3-5 years): Integrate the system with a cloud-based maintenance planning platform, enabling automated scheduling of maintenance activities and optimizing drone fleet utilization. Explore advanced federated learning techniques like personalization and edge device intelligence.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Conclusion&lt;br&gt;
This research introduces a novel framework for drone motor control system management, combining federated learning and Gaussian Process Regression for enhanced anomaly detection, accurate RUL prediction, and data privacy preservation. The proposed system offers significant advantages over existing methods and is poised to revolutionize drone fleet management, driving operational efficiency and reducing maintenance costs. This approach is immediately commercializable and provides a scalable foundation for future advancements in drone technology and predictive maintenance.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Mathematical Details &amp;amp; Appendices (Further detailing GPR equations, FedAvg algorithm pseudocode, and kernel parameter optimization) – Omitted for brevity, available upon request.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Commentary&lt;br&gt;
Explanatory Commentary: Automated Drone Motor Maintenance with Federated Learning &amp;amp; Gaussian Processes&lt;/p&gt;
&lt;p&gt;This research tackles a growing problem in the drone industry: ensuring reliable operation and minimizing costly motor failures. Drones are increasingly used in everything from package delivery to agricultural inspections, making their performance vital. Unexpected motor breakdowns lead to downtime, potential damage, and safety risks. This study proposes a smart system that uses advanced technology to predict when drone motors will need maintenance, before failures occur – a process known as predictive maintenance. It achieves this using a combination of federated learning and Gaussian Process Regression (GPR).&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Research Topic Explanation and Analysis&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The core challenge isn't just predicting failures; it’s doing so while protecting sensitive data. Traditional approaches collect all drone flight data in one central location. This raises privacy concerns and makes it difficult to adapt the system to the unique operating conditions of each drone. Imagine a drone working in a hot desert versus one operating in a cold, rainy environment; their motor wear patterns will be very different. Federated learning solves this data privacy dilemma and adaptability issue.&lt;/p&gt;
&lt;p&gt;Federated Learning: Decentralized Intelligence. Think of it like this: instead of everyone sending their data to a central brain, each drone has its own small brain (a local model) that learns from its own experiences. These &quot;brains&quot; regularly communicate with a central coordinator (the Federated Learning Server or FLS), sharing only how they’ve learned (model updates, not the raw data itself). The coordinator then combines these updates to create an improved overall &quot;brain&quot; for the whole fleet. This protects individual drone data while still allowing the fleet to benefit from collective learning. It’s like a group discussing ideas without revealing their personal notes.&lt;/p&gt;
&lt;p&gt;Gaussian Process Regression (GPR): Predicting the Future. GPR is a specialized tool for making predictions with uncertainty. Unlike simpler models that just give a single predicted value, GPR provides a range of possible values, along with an estimate of how confident it is in each prediction. In this context, GPR models the degradation process of a motor. It learns from the historical sensor data (current, voltage, temperature, RPM, vibration) and then uses this knowledge to predict how the motor's performance will change over time, effectively forecasting its RUL – Remaining Useful Life. The uncertainty information is crucial; if GPR is unsure, it flags the motor for inspection.&lt;/p&gt;
&lt;p&gt;Key Question &amp;amp; Technical Advantages: The central question is: Can we accurately predict motor failure while keeping drone data private and adaptable to varied conditions? This research's advantage lies in this combined approach. Centralized methods are privacy-invasive, while independent GPR models on each drone lack the benefits of shared knowledge. Federated learning paired with GPR offers the best of both worlds: privacy and collective intelligence. A limitation is the computational burden on each drone for local GPR training and the communication overhead between drones and the FLS.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Mathematical Model and Algorithm Explanation&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The algorithm at the heart of this system is a modified version of FedAvg (Federated Averaging). The goal is to find the best global GPR model.&lt;/p&gt;
&lt;p&gt;Local Training: Each drone uses sensor data to train a GPR model. The GPR model uses a Matérn kernel, a mathematical function describing how data points relate to each other. Think of it like a recipe—the kernel defines the ingredients and mixing process for making predictions. The Matérn kernel is flexible, allowing it to capture varying levels of smoothness in the motor degradation process; some motors wear gradually, others more abruptly. The training uses stochastic gradient descent (SGD), a technique involving iteratively refining the GPR model’s parameters to better fit the sensor data. It’s like slowly adjusting a radio dial until you get a clear signal.&lt;/p&gt;
&lt;p&gt;Weighted Averaging: The FLS combines model updates from each drone. Not all drones are created equal: some might experience more extreme conditions or have better sensors, so their data is given more weight in the aggregation process. The weight assigned to each drone's model is inversely proportional to the variance (spread) of its local data; drones with more consistent data have higher weights. This dynamic weighting is a key innovation. Mathematically, w_i = 1 / (local_data_variance_i), where w_i is the weight assigned to drone i and local_data_variance_i is the variance of its local data.&lt;/p&gt;
&lt;p&gt;Iteration: The process repeats—local training, global aggregation—for a set number of rounds, gradually improving the global GPR model’s accuracy.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Experiment and Data Analysis Method&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;To test the system, the researchers created a simulated dataset—a &quot;digital twin&quot; of a drone motor. This means they built a computer model that accurately mimics a real motor’s behavior, including the gradual degradation caused by wear, friction, and corrosion. The dataset comprises 100 drones, each operating under various simulated environmental conditions.&lt;/p&gt;
&lt;p&gt;Experimental Setup Description: The &quot;physics-based motor model&quot; simulates the factors affecting motor life. Advanced terminology like “wear, friction, and corrosion” are modeled through equations that describe how forces and material properties change over time. This ensures the data reflects realistic degradation patterns. The experiment involves running the federated learning algorithm with the simulated data and then comparing the results against two baseline methods: a traditional centralized GPR and individual GPR models on each drone.&lt;/p&gt;
&lt;p&gt;Data Analysis Techniques: The researchers used several metrics to evaluate performance:&lt;/p&gt;
&lt;p&gt;Anomaly Detection Accuracy: Measured by Precision, Recall, and F1-score—how well the system identifies true anomalies and avoids false alarms.&lt;/p&gt;
&lt;p&gt;RUL Prediction Error: Measured by Root Mean Squared Error (RMSE)—how closely the predicted RUL matches the actual RUL. A lower RMSE indicates better prediction accuracy.&lt;/p&gt;
&lt;p&gt;Communication Overhead: Number of communication rounds to reach convergence, vital for efficiency.&lt;/p&gt;
&lt;p&gt;Privacy Preservation: Differential Privacy parameter (epsilon, delta)—lower values demonstrating stronger data protection. Statistical analysis compared models, and regression analysis identified the relationships between sensor readings and degradation patterns.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Research Results and Practicality Demonstration&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The results showed that the federated learning + GPR system outperformed the baselines in both anomaly detection and RUL prediction. The adaptive weighting scheme helped the system converge faster (using fewer communication rounds) and achieve higher accuracy.&lt;/p&gt;
&lt;p&gt;Results Explanation &amp;amp; Visual Representation: The researchers predict an F1-score of 0.95 for anomaly detection, meaning the system correctly identifies approximately 95% of the true motor anomalies. The RMSE for RUL prediction is 5%, indicating a respectable degree of accurate prediction.&lt;/p&gt;
&lt;p&gt;Practicality Demonstration: The estimated reduction in maintenance costs (15-20%) and an improvement in operational reliability (5-10%) provide a strong business case. The immediate commercial value is enhanced fleet management. Imagine an airline using this system to predict maintenance needs for their drone fleet inspecting aircraft—reducing downtime and costs significantly.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Verification Elements and Technical Explanation&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The researchers rigorously validated their system. The physics-based motor model ensures that the simulated data accurately replicates real-world conditions. The dynamic weighting scheme implemented in FedAvg was tested to specifically determine that less variance results in higher model accuracy than a system that does not weight the data accordingly. These weights prioritized drones exhibiting consistent degradation patterns.&lt;/p&gt;
&lt;p&gt;Verification Process: By comparing the performance of the federated learning system with the centralized and independent GPR models, they demonstrated that federated learning consistently delivered superior results while protecting data privacy.&lt;/p&gt;
&lt;p&gt;Technical Reliability: The FedAvg algorithm, while modified, is a well-established method in federated learning. The added adaptation ensures model accuracy and stability during forecasting.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Adding Technical Depth&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This research builds on the existing body of work in federated learning and GPR, but specifically advances it by integrating these techniques in a targeted way for predictive maintenance. Previous federated learning applications often used simpler anomaly detection methods. The innovation lies in the combination of federated learning with the sophisticated and accurate predictive capabilities of GPR. The adaptive weighting scheme differentiates this study from standard FedAvg, which generally weights all drones equally. Further, it employs a Matérn Kernel, which accounts for variance during the training of the data.&lt;/p&gt;
&lt;p&gt;Technical Contribution: The differentiation lies in contributing a novel federated learning approach coupled with GPR, specifically tailored to drone motor predictive maintenance. The adaptive weighting scheme directly addresses the challenges of data heterogeneity and varying data quality across drones within a fleet. This extends the reach of federated learning to more complex predictive tasks, proving its real-world application in time-critical fields like drone operations.&lt;/p&gt;
&lt;p&gt;Conclusion:&lt;/p&gt;
&lt;p&gt;This study presents a compelling solution for optimizing drone maintenance, balancing accuracy, privacy, and practicality. With real-world applications ranging from logistics to infrastructure inspection, this federated learning and GPR-powered system promises to revolutionize drone fleet management, paving the way for safer, more efficient, and cost-effective drone operations.&lt;/p&gt;
&lt;p&gt;This document is a part of the Freederia Research Archive. Explore our complete collection of advanced research at en.freederia.com, or visit our main portal at freederia.com to learn more about our mission and other initiatives.&lt;/p&gt;
</description>
<category>Machine Learning</category>
<guid isPermaLink="true">https://coderlegion.com/4499/automated-anomaly-detection-and-predictive-maintenance-in-drone-motor-control-systems</guid>
<pubDate>Fri, 08 Aug 2025 07:06:32 +0000</pubDate>
</item>
<item>
<title>How to apply various postprocessing effects like Bloom, Motion Blur, and Pixelation inside a Vue</title>
<link>https://coderlegion.com/3744/how-apply-various-postprocessing-effects-like-bloom-motion-blur-and-pixelation-inside-vue</link>
<description>&lt;p&gt;Want to bring &lt;strong&gt;cinematic 3D effects&lt;/strong&gt; to your Vue app? Enter &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://tresjs.org&quot;&gt;TresJS&lt;/a&gt;&lt;/strong&gt;—a powerful, declarative wrapper around Three.js that plays beautifully with Vue 3's Composition API.&lt;/p&gt;
&lt;p&gt;If you’ve ever dreamed of adding &lt;strong&gt;Bloom&lt;/strong&gt;, &lt;strong&gt;Pixelation&lt;/strong&gt;, &lt;strong&gt;Motion Blur&lt;/strong&gt;, or even &lt;strong&gt;Glitch effects&lt;/strong&gt; with just a few lines of code—TresJS makes it possible.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;setup&quot;&gt;Setup&lt;/h3&gt;
&lt;p&gt;First, install TresJS and its core dependencies:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install @tresjs/core three @tresjs/post-processing
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you're using Vite, you're ready to go. Make sure you’re on &lt;strong&gt;Vue 3&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;example-1-glowing-sphere-with-bloom&quot;&gt;Example 1: Glowing Sphere with Bloom&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;!-- GlowingSphere.vue --&amp;gt;
&amp;lt;template&amp;gt;
  &amp;lt;Canvas clear-color=&quot;#000&quot;&amp;gt;
    &amp;lt;TresPerspectiveCamera :position=&quot;[0, 0, 5]&quot; /&amp;gt;
    &amp;lt;TresAmbientLight /&amp;gt;
    &amp;lt;TresPointLight :intensity=&quot;1.5&quot; :position=&quot;[5, 5, 5]&quot; /&amp;gt;

    &amp;lt;TresMesh&amp;gt;
      &amp;lt;TresSphereGeometry :args=&quot;[1, 32, 32]&quot; /&amp;gt;
      &amp;lt;TresMeshStandardMaterial color=&quot;hotpink&quot; emissive=&quot;hotpink&quot; emissiveIntensity=&quot;1&quot; /&amp;gt;
    &amp;lt;/TresMesh&amp;gt;

    &amp;lt;EffectComposer&amp;gt;
      &amp;lt;Bloom :intensity=&quot;1.5&quot; :luminanceThreshold=&quot;0.4&quot; /&amp;gt;
    &amp;lt;/EffectComposer&amp;gt;
  &amp;lt;/Canvas&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; A smooth glowing neon sphere on a black background—great for sci-fi UIs.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;example-2-motion-blur-for-fast-movement&quot;&gt;Example 2: Motion Blur for Fast Movement&lt;/h3&gt;
&lt;p&gt;Motion blur works best with animation. Here’s a spinning cube with blur.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;!-- SpinningCube.vue --&amp;gt;
&amp;lt;template&amp;gt;
  &amp;lt;Canvas&amp;gt;
    &amp;lt;TresPerspectiveCamera :position=&quot;[0, 0, 6]&quot; /&amp;gt;
    &amp;lt;TresAmbientLight /&amp;gt;

    &amp;lt;TresMesh ref=&quot;cube&quot; :rotation=&quot;rotation&quot;&amp;gt;
      &amp;lt;TresBoxGeometry /&amp;gt;
      &amp;lt;TresMeshStandardMaterial color=&quot;#00ffff&quot; /&amp;gt;
    &amp;lt;/TresMesh&amp;gt;

    &amp;lt;EffectComposer&amp;gt;
      &amp;lt;MotionBlur :samples=&quot;32&quot; /&amp;gt;
    &amp;lt;/EffectComposer&amp;gt;
  &amp;lt;/Canvas&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup&amp;gt;
import { ref, onMounted } from 'vue'
import { useRenderLoop } from '@tresjs/core'

const rotation = ref([0, 0, 0])

useRenderLoop(({ delta }) =&amp;gt; {
  rotation.value[0] += delta
  rotation.value[1] += delta * 1.2
})
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; A smoothly rotating cube with natural blur on edges—great for dynamic UIs.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;example-3-pixelation-effect-retro-style-&quot;&gt;Example 3: Pixelation Effect (Retro Style)&lt;/h3&gt;
&lt;p&gt;Perfect for game UIs or retro visuals.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;!-- PixelatedScene.vue --&amp;gt;
&amp;lt;template&amp;gt;
  &amp;lt;Canvas&amp;gt;
    &amp;lt;TresPerspectiveCamera :position=&quot;[0, 0, 5]&quot; /&amp;gt;
    &amp;lt;TresAmbientLight /&amp;gt;
    
    &amp;lt;TresMesh&amp;gt;
      &amp;lt;TresTorusKnotGeometry :args=&quot;[1, 0.3, 100, 16]&quot; /&amp;gt;
      &amp;lt;TresMeshStandardMaterial color=&quot;limegreen&quot; /&amp;gt;
    &amp;lt;/TresMesh&amp;gt;

    &amp;lt;EffectComposer&amp;gt;
      &amp;lt;Pixelation :granularity=&quot;5&quot; /&amp;gt;
    &amp;lt;/EffectComposer&amp;gt;
  &amp;lt;/Canvas&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; The entire scene appears in blocky, pixelated chunks—a nostalgic, game-like vibe.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;stacking-multiple-effects&quot;&gt;Stacking Multiple Effects&lt;/h3&gt;
&lt;p&gt;Yes, you can combine them!&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-vue&quot;&gt;&amp;lt;EffectComposer&amp;gt;
  &amp;lt;Bloom :intensity=&quot;1.2&quot; /&amp;gt;
  &amp;lt;Noise opacity=&quot;0.05&quot; /&amp;gt;
  &amp;lt;Vignette eskil=&quot;false&quot; offset=&quot;0.5&quot; darkness=&quot;0.8&quot; /&amp;gt;
&amp;lt;/EffectComposer&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This stack creates a subtle cinematic atmosphere: a glowing bloom, grainy texture, and dark edges—like a movie scene.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;dev-tips&quot;&gt;Dev Tips&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Performance:&lt;/strong&gt; Effects like bloom and motion blur can be GPU-heavy. Test on multiple devices.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Declarative FTW:&lt;/strong&gt; TresJS uses Vue’s reactivity, so animating or toggling effects is dead simple.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Debug:&lt;/strong&gt; Use &lt;code&gt;&amp;lt;Stats /&amp;gt;&lt;/code&gt; from TresJS for FPS monitoring.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;final-thoughts&quot;&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;TresJS makes high-end visuals &lt;strong&gt;accessible to frontend devs&lt;/strong&gt; without needing to dive deep into raw WebGL or complex shaders. Whether you're building a dashboard, a product showcase, or an immersive landing page, &lt;strong&gt;effects like bloom, blur, and pixelation&lt;/strong&gt; can instantly make your app stand out.&lt;/p&gt;
&lt;p&gt;If you're already in the Vue ecosystem and want cinematic 3D without the Three.js boilerplate—&lt;strong&gt;TresJS is your toolkit&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
</description>
<category>Frontend Dev</category>
<guid isPermaLink="true">https://coderlegion.com/3744/how-apply-various-postprocessing-effects-like-bloom-motion-blur-and-pixelation-inside-vue</guid>
<pubDate>Mon, 07 Jul 2025 02:09:36 +0000</pubDate>
</item>
<item>
<title>Clean Async State Management with Pinia Colada</title>
<link>https://coderlegion.com/3728/clean-async-state-management-with-pinia-colada</link>
<description>&lt;hr&gt;
&lt;p&gt;Managing async state—think loading spinners, error handling, stale data—is notoriously messy. If you’ve wrestled with nested subscriptions or redundant fetch logic in Vue+Pinia apps, &lt;strong&gt;Pinia Colada&lt;/strong&gt; might be the game-changer you need.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-is-pinia-colada-&quot;&gt;What Is Pinia Colada?&lt;/h3&gt;
&lt;p&gt;Pinia Colada is a powerful data-fetching layer built on top of Pinia. It eliminates boilerplate by providing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Automatic &lt;strong&gt;caching&lt;/strong&gt; and &lt;strong&gt;deduplication&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Smart query &lt;strong&gt;invalidation&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Declarative API for &lt;strong&gt;queries&lt;/strong&gt; (data fetches) and &lt;strong&gt;mutations&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Support for &lt;strong&gt;optimistic updates&lt;/strong&gt;, SSR, and tree-shaken size of \~2 KB&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;quick-start&quot;&gt;Quick Start&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm install pinia @pinia/colada
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then in your Vue app setup:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;import { createPinia } from 'pinia'
import { PiniaColada } from '@pinia/colada'

app.use(createPinia())
app.use(PiniaColada, {/* optional plugins */})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now you’re ready to use &lt;code&gt;useQuery&lt;/code&gt; and &lt;code&gt;useMutation&lt;/code&gt; everywhere.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;example-querying-data&quot;&gt;Example: Querying Data&lt;/h3&gt;
&lt;p&gt;Instead of manual loading flags, error state, and fetch logic:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const { data, error, isPending, isLoading, refresh } = useQuery({
  key: ['users'],
  query: fetchUsers,
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then in the template, you can declaratively handle the three states:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;isPending&lt;/code&gt; — first fetch&lt;/li&gt;
&lt;li&gt;&lt;code&gt;isLoading&lt;/code&gt; — refreshes&lt;/li&gt;
&lt;li&gt;&lt;code&gt;error&lt;/code&gt; — if broken&lt;/li&gt;
&lt;li&gt;&lt;code&gt;data&lt;/code&gt; — once resolved&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;mutations-amp-invalidation&quot;&gt;Mutations &amp;amp; Invalidation&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;const { mutate: addComment, isLoading } = useMutation({
  mutation: postComment,
  onSettled: async ({ id }) =&amp;gt; {
    await queryCache.invalidateQueries({ key: ['comments', id], exact: true })
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This lets you optimistically update and ensure fresh data before closing modal dialogs or user notifications.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-you-ll-love-it&quot;&gt;Why You’ll Love It&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Minimal state boilerplate: No more juggling &lt;code&gt;isLoading&lt;/code&gt;, &lt;code&gt;isError&lt;/code&gt;, and &lt;code&gt;data&lt;/code&gt; manually.&lt;/li&gt;
&lt;li&gt;Built-in caching and deduplication for better UX and efficiency.&lt;/li&gt;
&lt;li&gt;Fully typed, tree-shakeable, zero deps (besides Pinia), and SSR-ready.&lt;/li&gt;
&lt;li&gt;Plugins like &lt;strong&gt;autoRefetch&lt;/strong&gt; and &lt;strong&gt;delay&lt;/strong&gt; help avoid flickers or stale data.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;real-world-feedback&quot;&gt;Real-World Feedback&lt;/h3&gt;
&lt;p&gt;Reddit users highlight Pinia Colada's advantage for API-driven state:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“This is what Pinia stores call service methods… API calls inside sweet local store. It’s perfect to avoid multiple fetches.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Others note:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“Pinia is for global app state. For API caching use Tanstack Query… Pinia Colada consolidates the conundrum.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;tl-dr-when-to-use-it&quot;&gt;TL;DR – When to Use It&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Use Pinia Colada when&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You want clean, reusable logic for API calls&lt;/li&gt;
&lt;li&gt;You care about efficient caching and UX consistency&lt;/li&gt;
&lt;li&gt;You prefer declarative state over imperative patterns&lt;/li&gt;
&lt;li&gt;You work with mutable server data and want stale-data control&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;want-to-go-further-&quot;&gt;Want to Go Further?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Explore &lt;strong&gt;optimistic updates&lt;/strong&gt;, SSR support, and GraphQL usage&lt;/li&gt;
&lt;li&gt;Combine with Vue Router Data Loaders and modular async handling&lt;/li&gt;
&lt;li&gt;Try plugins like auto-refetch and delay to polish UX&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;Pinia Colada brings a modern, well-structured approach to async state in Vue apps—cutting boilerplate, reducing bugs, and improving developer happiness. Whether it's simple CRUD or complex dialogs or pagination, it's well worth a look.&lt;/p&gt;
&lt;hr&gt;
</description>
<category>Frontend Dev</category>
<guid isPermaLink="true">https://coderlegion.com/3728/clean-async-state-management-with-pinia-colada</guid>
<pubDate>Sat, 05 Jul 2025 08:33:04 +0000</pubDate>
</item>
<item>
<title>Turn Your Vue Web App into a Mobile App (Android &amp; iOS) in Just 5 Minutes</title>
<link>https://coderlegion.com/3431/turn-your-vue-web-app-into-a-mobile-app-android-ios-in-just-5-minutes</link>
<description>&lt;p&gt;In this quick tutorial, I’ll show you how to use CapacitorJS to convert your existing Vue.js web app into a fully functional mobile app for Android and iOS — without rewriting your codebase.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://youtu.be/ursT2sV3Hq8?si=BkmVvXcsWxGe0-Ph&quot;&gt;▶️ Watch the full tutorial here&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This method is the fastest way to bring your frontend skills to mobile platforms using tools you already know.&lt;/p&gt;
&lt;p&gt;Commands Used in This Video:&lt;br&gt;
Install the Capacitor CLI locally&lt;/p&gt;
&lt;p&gt;npm install -D @capacitor/cli&lt;br&gt;
Initialize Capacitor in your Vue project&lt;/p&gt;
&lt;p&gt;npx cap init&lt;br&gt;
Install the required packages&lt;/p&gt;
&lt;p&gt;npm install @capacitor/core @capacitor/ios @capacitor/android&lt;br&gt;
Add native platforms&lt;/p&gt;
&lt;p&gt;npx cap add ios&lt;br&gt;
npx cap add android&lt;br&gt;
Build and sync your app&lt;/p&gt;
&lt;p&gt;npm run build&lt;br&gt;
npx cap sync&lt;br&gt;
Open the native projects in your IDE&lt;/p&gt;
&lt;p&gt;npx cap open ios&lt;br&gt;
npx cap open android&lt;br&gt;
What is CapacitorJS and Why Use It?&lt;br&gt;
CapacitorJS is a modern open-source native runtime developed by the Ionic team. It allows developers to build cross-platform mobile apps using web technologies like HTML, CSS, and JavaScript — without needing to rewrite your app in native languages such as Swift or Kotlin.&lt;/p&gt;
&lt;p&gt;With Capacitor, your web app runs inside a native WebView container, enabling it to function like a true mobile app. Beyond just wrapping your app, Capacitor provides easy access to native device features like the camera, filesystem, geolocation, push notifications, and more through a rich plugin ecosystem.&lt;/p&gt;
&lt;p&gt;Cross-Framework Compatibility&lt;br&gt;
One of Capacitor’s biggest strengths is that it works with any modern JavaScript framework. Whether you are using:&lt;/p&gt;
&lt;p&gt;Vue.js (like in this tutorial)&lt;br&gt;
React&lt;br&gt;
Angular&lt;br&gt;
Svelte&lt;br&gt;
Or even plain vanilla JavaScript&lt;br&gt;
Capacitor can seamlessly integrate with your existing project. This flexibility means you don’t need to change your frontend framework or rewrite your codebase to build mobile apps — just add Capacitor and you’re good to go.&lt;/p&gt;
&lt;p&gt;Why Choose Capacitor Over Other Solutions?&lt;br&gt;
Compared to alternatives like Cordova, Capacitor is:&lt;/p&gt;
&lt;p&gt;More modern and lightweight, built with a focus on web standards and developer experience&lt;br&gt;
Better integrated with native SDKs, giving you easier access to device APIs&lt;br&gt;
Supports live reload and faster development cycles&lt;br&gt;
Actively maintained and backed by the Ionic team, ensuring ongoing support and improvements&lt;br&gt;
How Capacitor Works Under the Hood&lt;br&gt;
Capacitor takes your built web app (the static files you generate when you run npm run build) and packages it inside a native project shell for iOS and Android. This native shell includes:&lt;/p&gt;
&lt;p&gt;A WebView component that loads your web app&lt;br&gt;
A bridge that connects JavaScript code to native device APIs&lt;br&gt;
Native projects that you can open and customize in Xcode (for iOS) or Android Studio (for Android)&lt;br&gt;
This means your app behaves just like a native app on mobile devices but allows you to write your entire UI and logic in web languages.&lt;/p&gt;
&lt;p&gt;What You Need to Get Started&lt;br&gt;
Your web app ready to build (Vue, React, Angular, or vanilla JS)&lt;br&gt;
Node.js and npm installed on your system&lt;br&gt;
Android Studio and/or Xcode installed for running native projects&lt;br&gt;
Basic familiarity with command-line tools&lt;br&gt;
Wrapping Up&lt;br&gt;
Using CapacitorJS, you can quickly turn your existing web app into a mobile app without the overhead of learning native development. This is a huge advantage for web developers looking to expand their reach to mobile users efficiently.&lt;/p&gt;
&lt;p&gt;If you’re comfortable building with Vue (or any JavaScript framework), adding Capacitor to your workflow will open up new possibilities for app distribution on mobile platforms — faster and easier than ever before.&lt;/p&gt;
</description>
<category>Vue Components</category>
<guid isPermaLink="true">https://coderlegion.com/3431/turn-your-vue-web-app-into-a-mobile-app-android-ios-in-just-5-minutes</guid>
<pubDate>Mon, 23 Jun 2025 15:49:53 +0000</pubDate>
</item>
<item>
<title>How I Built a Smart Elevator Control System in Vue 3 and TypeScript</title>
<link>https://coderlegion.com/3371/how-i-built-a-smart-elevator-control-system-in-vue-3-and-typescript</link>
<description>&lt;p&gt;Have you ever wondered how elevator systems operate behind the scenes? I recently developed a real-time elevator control simulation using Vue 3, TypeScript, and Tailwind CSS a project that brings the complexity of multi-elevator coordination to life through a modern web interface.&lt;/p&gt;
&lt;p&gt;In this post, I’ll walk you through the architecture, implementation details, and key lessons learned while building this interactive simulation from the ground up.&lt;/p&gt;
&lt;p&gt;Project Overview&lt;br&gt;
This simulation models a 10-floor building served by 4 independently operating elevators. Each elevator intelligently picks up passengers, navigates between floors, and drops them off at their destination all in real time with state updates every 10 seconds.&lt;/p&gt;
&lt;p&gt;Live Demo Highlights&lt;br&gt;
Four elevators running concurrently&lt;br&gt;
Real-time passenger pickup &amp;amp; drop-off&lt;br&gt;
Dynamic floor request generation&lt;br&gt;
Visual floor indicators with request tracking&lt;br&gt;
Scrollable, real-time activity logs&lt;br&gt;
Modern, responsive user interface&lt;br&gt;
Tech Stack&lt;br&gt;
Frontend Framework: Vue 3 + Composition API&lt;br&gt;
Language: TypeScript&lt;br&gt;
Styling: Tailwind CSS&lt;br&gt;
Build Tool: Vite&lt;br&gt;
State Management: Vue’s built-in reactivity &amp;amp; composables&lt;br&gt;
System Architecture&lt;br&gt;
Core Data Structures&lt;br&gt;
To ensure clarity and type safety, the system relies on three main TypeScript interfaces:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    src/
├── components/
│   ├── ElevatorCard.vue
│   └── elevator/
│       ├── ElevatorStatus.vue
│       ├── FloorDisplay.vue
│       ├── PassengerInfo.vue
│       └── ElevatorLogs.vue
├── composables/
│   └── useElevatorSystem.ts
├── utils/
│   └── elevatorUtils.ts
└── types/
    └── elevator.ts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Key Implementation Details&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Elevator Movement Logic&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A basic but effective algorithm handles direction reversal and floor progression:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    function moveElevator(elevator: Elevator) {
  if (elevator.direction === 'up') {
    if (elevator.currentFloor &amp;lt; elevator.flors.length) {
      elevator.currentFloor++
      log(elevator, `Moved up to floor ${elevator.currentFloor}`)
    } else {
      elevator.direction = 'down'
      elevator.currentFloor--
      log(elevator, `Reached top. Reversing to down → floor ${elevator.currentFloor}`)
    }
  } else {
    if (elevator.currentFloor &amp;gt; 1) {
      elevator.currentFloor--
      log(elevator, `Moved down to floor ${elevator.currentFloor}`)
    } else {
      elevator.direction = 'up'
      elevator.currentFloor++
      log(elevator, `Reached bottom. Reversing to up → floor ${elevator.currentFloor}`)
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Passenger Pickup &amp;amp; Drop-Off&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Passengers are generated and assigned direction-based destinations:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    function handlePickup(elevator: Elevator, floor: Floor) {
  const waitingPassengers = floor.up || floor.down ? 1 : 0
  if (waitingPassengers &amp;gt; 0) {
    const newPassenger: Passenger = {
      id: Date.now(),
      destinationFloor: Math.floor(Math.random() * 10) + 1,
      direction: floor.up ? 'up' : 'down'
    }

    if (floor.up) {
      while (newPassenger.destinationFloor &amp;lt;= elevator.currentFloor) {
        newPassenger.destinationFloor = Math.floor(Math.random() * 10) + 1
      }
    }

    elevator.passengers.push(newPassenger)
    log(elevator, `Picked up passenger going to floor ${newPassenger.destinationFloor}`)
    floor.up = false
    floor.down = false
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;System Controller&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The main composable manages the elevator system in real time:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    function useElevatorSystem(totalFloors: number = 10, maxPassengers: number = 5) {
  const elevators = ref&amp;lt;Elevator[]&amp;gt;([])
  let moveIntervalId: ReturnType&amp;lt;typeof setInterval&amp;gt; | null = null

  function startSystem() {
    moveIntervalId = setInterval(() =&amp;gt; {
      elevators.value.forEach(elevator =&amp;gt; {
        dropOffPassengers(elevator)
        moveElevator(elevator)

        const currentFloorObj = elevator.flors.find(f =&amp;gt; f.id === elevator.currentFloor)
        if (currentFloorObj &amp;amp;&amp;amp; (currentFloorObj.up || currentFloorObj.down)) {
          handlePickup(elevator, currentFloorObj)
        }

        dropOffPassengers(elevator)
      })

      elevators.value.forEach(generateRandomFloorRequest)
    }, 10000)
  }

  return { elevators, initializeElevators, startSystem, stopSystem }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;User Interface Design&lt;/p&gt;
&lt;p&gt;Floor Display&lt;/p&gt;
&lt;p&gt;Floor number with a circular badge&lt;/p&gt;
&lt;p&gt;Highlighted active floor&lt;/p&gt;
&lt;p&gt;Up/down request indicators&lt;/p&gt;
&lt;p&gt;Clear floor labeling&lt;/p&gt;
&lt;p&gt;Real-Time Feedback&lt;/p&gt;
&lt;p&gt;Live updates for each elevator&lt;/p&gt;
&lt;p&gt;Passenger count &amp;amp; destinations&lt;/p&gt;
&lt;p&gt;Scrollable log with timestamps&lt;/p&gt;
&lt;p&gt;Animated visual cues for movement&lt;/p&gt;
&lt;p&gt;Challenges &amp;amp; Solutions&lt;/p&gt;
&lt;p&gt;State Management Complexity&lt;/p&gt;
&lt;p&gt;Managing 4 elevators with independent logic and shared global state was no small feat.&lt;/p&gt;
&lt;p&gt;Solution: Leveraged Vue 3’s Composition API and ref/reactive to create performant, scoped reactivity.&lt;/p&gt;
&lt;p&gt;Real-Time Synchronization&lt;/p&gt;
&lt;p&gt;Ensuring consistent updates while keeping elevator logic independent.&lt;/p&gt;
&lt;p&gt;Solution: I used a centralized interval loop to orchestrate operations while preserving elevator autonomy.&lt;/p&gt;
&lt;p&gt;Type Safety&lt;/p&gt;
&lt;p&gt;Elevators, floors, and passengers required intricate interrelations.&lt;/p&gt;
&lt;p&gt;Solution: Strongly typed interfaces allowed safe interaction across components and composables.&lt;/p&gt;
&lt;p&gt;Performance Highlights&lt;/p&gt;
&lt;p&gt;Reactive efficiency via scoped updates&lt;/p&gt;
&lt;p&gt;Efficient log storage with unshift() for quick inserts&lt;/p&gt;
&lt;p&gt;Resource cleanup with interval teardown on unmount&lt;/p&gt;
&lt;p&gt;Optimized rendering through component isolation&lt;/p&gt;
&lt;p&gt;What’s Next?&lt;/p&gt;
&lt;p&gt;The system offers exciting possibilities for further development:&lt;/p&gt;
&lt;p&gt;Passenger Priorities: VIP or emergency overrides&lt;/p&gt;
&lt;p&gt;Energy Efficiency: Smarter routing logic&lt;/p&gt;
&lt;p&gt;Multi-Building Support: Shared elevator infrastructure&lt;/p&gt;
&lt;p&gt;Analytics: Real-time performance dashboards&lt;/p&gt;
&lt;p&gt;Interactive Mode: Manual elevator control by users&lt;/p&gt;
&lt;p&gt;Lessons Learned&lt;/p&gt;
&lt;p&gt;Composables = Clean architecture&lt;/p&gt;
&lt;p&gt;TypeScript = Fewer bugs&lt;/p&gt;
&lt;p&gt;Timing systems demand precision&lt;/p&gt;
&lt;p&gt;Visual feedback = Better UX&lt;/p&gt;
&lt;p&gt;Logging simplifies debugging &amp;amp; UX clarity&lt;/p&gt;
&lt;p&gt;Conclusion&lt;/p&gt;
&lt;p&gt;This project was a rewarding challenge that merged real-time systems thinking with modern frontend development. Vue 3’s reactivity, TypeScript’s type assurance, and Tailwind’s rapid styling capabilities came together to create a clean, scalable, and visually engaging simulation.&lt;/p&gt;
&lt;p&gt;If you're passionate about algorithms, state management, or interactive web interfaces, I highly encourage exploring or extending this project.&lt;/p&gt;
&lt;p&gt;Source Code&lt;/p&gt;
&lt;p&gt;The full code is available on GitHub. Feel free to fork it, experiment with new features, or optimize the algorithms!&lt;/p&gt;
&lt;p&gt;Project Links:&lt;/p&gt;
&lt;p&gt;Live Demo: &lt;a rel=&quot;nofollow&quot; href=&quot;https://toninaumoski.github.io/smartelevatorcontrolsystem/&quot;&gt;View Project&lt;/a&gt; &lt;/p&gt;
&lt;p&gt;GitHub Repository: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ToniNaumoski/Smart-Elevator-Control-System&quot;&gt;Smart-Elevator-Control-System&lt;/a&gt;&lt;/p&gt;
</description>
<category>Web Dev</category>
<guid isPermaLink="true">https://coderlegion.com/3371/how-i-built-a-smart-elevator-control-system-in-vue-3-and-typescript</guid>
<pubDate>Fri, 20 Jun 2025 01:23:32 +0000</pubDate>
</item>
<item>
<title>How to create admin panel in a minutes</title>
<link>https://coderlegion.com/3028/how-to-create-admin-panel-in-a-minutes</link>
<description>&lt;p&gt;I will show you how to easily create an extendable admin panel using the open-source (and free!) AdminForth framework. The setup process is incredibly simple and takes just a few minutes.&lt;/p&gt;
&lt;h3 id=&quot;requirements-1&quot;&gt;Requirements&lt;/h3&gt;
&lt;p&gt;All you need is Node.js version 20 or later installed.&lt;/p&gt;
&lt;h3 id=&quot;let-s-get-started&quot;&gt;Let's get started&lt;/h3&gt;
&lt;p&gt;Run:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npx adminforth create-app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will launch a small utility that will ask you to name your project.&lt;br&gt;
Then, it will prompt you for your database URL in the field &lt;code&gt;&quot;Please specify the database URL to use &amp;gt;&quot;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;You can use SQLite, Postgres, MySQL, MongoDB, or ClickHouse URLs.&lt;br&gt;
For testing purposes, we recommend SQLite, as it doesn't require a daemon and works out of the box.&lt;/p&gt;
&lt;p&gt;Once the app is created, you should see:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=1577179538893905645&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;next-steps-prepare-and-run-the-app&quot;&gt;Next steps: prepare and run the app&lt;/h3&gt;
&lt;p&gt;Run the suggested commands:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd adminforth-app
npm run makemigration -- --name init &amp;amp;&amp;amp; npm run migrate:local
npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's it! Open &lt;a rel=&quot;nofollow&quot; href=&quot;http://localhost:3500/&quot;&gt;http://localhost:3500/&lt;/a&gt; in your browser and you should see:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=7207669731169366153&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Login using &lt;code&gt;adminforth&lt;/code&gt; as both username and password.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;⚠️ In production, be sure to change the password for this user via the admin panel.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;You will now see a list of admin users:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=18199405401752693134&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Try adding a user, and you can also test logging in with that user from an incognito window.&lt;br&gt;
Feel free to play around — try filters, navigation, and more.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;how-to-add-a-table-to-adminforth-admin-&quot;&gt;How to add a table to AdminForth Admin?&lt;/h2&gt;
&lt;p&gt;Now comes the most exciting part.&lt;br&gt;
By now, Prisma has generated a migration for the initial &lt;code&gt;adminuser&lt;/code&gt; table. But what if you want to manage additional tables from your existing database or add new ones? Let's do that.&lt;/p&gt;
&lt;p&gt;Let's add a new table to our SQLite database. Open your project in VS Code / Cursor:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;code .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=8094413108377506425&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Now, add the following code to your Prisma schema:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;model car {
  id            String   @id
  title         String
  max_speed     Int
  preview       String?   
  price         Decimal
  currency      String
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create and apply the migration:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm run makemigration -- --name add_car_model &amp;amp;&amp;amp; npm run migrate:local
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt; AdminForth is migration-agnostic — it never manages your database schema itself and can work with any database structure.&lt;br&gt;
For quick-start and testing purposes, the app created via &lt;code&gt;adminforth create-app&lt;/code&gt; bundles Prisma, which we used here for our SQLite schema.&lt;br&gt;
However, you're free to use any migration tool for your database. All AdminForth needs to know is the table and field names for the next step.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;define-the-resource&quot;&gt;Define the resource&lt;/h3&gt;
&lt;p&gt;Create a new resource file in the &lt;code&gt;resources&lt;/code&gt; folder named &lt;code&gt;car.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { AdminForthDataTypes, AdminForthResourceInput } from &quot;adminforth&quot;;
import { randomUUID } from &quot;crypto&quot;;


export default {
  table: &quot;car&quot;,
  resourceId: &quot;car&quot;,
  dataSource: &quot;maindb&quot;,
  columns: [
    {
      name: &quot;id&quot;,
      fillOnCreate: () =&amp;gt; randomUUID(),  // generate a random UUID when creating a new record in admin
      showIn: {
        create: false,
        list: false,
      }
    },
    {
      name: &quot;title&quot;,
      type: AdminForthDataTypes.STRING, // enforce STRING type (SQLite supports only TEXT, this ensures single-line input)
    },
    {
      name: &quot;max_speed&quot;,
      inputSuffix: &quot;km/h&quot;, // add a suffix in create/edit forms
    },
    {
      name: &quot;preview&quot;,
    },
    {
      name: &quot;price&quot;,
    },
    {
      name: &quot;currency&quot;,
      enum: [  // define an enumeration — AdminForth will automatically create a dropdown
        {
          label: &quot;Bitcoin&quot;,
          value: &quot;BTC&quot;,
        },
        {
          label: &quot;Tether&quot;,
          value: &quot;USDT&quot;,
        },
      ]
    }
  ]
} as AdminForthResourceInput;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now import this resource into &lt;code&gt;index.ts&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import carsResource from &quot;./resources/car.js&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And add it to the &lt;code&gt;resources&lt;/code&gt; list so AdminForth will know about it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  resources: [
    usersResource,
    carsResource, // added here
  ],
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, let's add a menu item so you can access the resource from the UI:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  menu: [
    { type: 'heading', label: 'SYSTEM' },
    {
      label: 'Users',
      icon: 'flowbite:user-solid',
      resourceId: 'adminuser'
    },
    // added this
    {
      label: 'Cars',
      icon: 'fa6-solid:car-side',
      resourceId: 'car'
    }
  ],
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And you're done!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=4676291766370977265&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Play around by adding your favorite cars and try filtering them.&lt;br&gt;
That's it for today! If you enjoyed this, feel free to leave a comment or reaction.&lt;br&gt;
Let me know if you'd like me to create more posts about extending and scaling your admin panel, adding powerful features like audit logs, file uploads, Google login, password reset, CSV import/export, and much more.&lt;/p&gt;
&lt;p&gt;I’m looking forward to your reactions! &lt;/p&gt;
</description>
<category>Getting Started</category>
<guid isPermaLink="true">https://coderlegion.com/3028/how-to-create-admin-panel-in-a-minutes</guid>
<pubDate>Wed, 14 May 2025 20:48:42 +0000</pubDate>
</item>
<item>
<title>How I Started Writing Unit Tests for Vue Components</title>
<link>https://coderlegion.com/1682/how-i-started-writing-unit-tests-for-vue-components</link>
<description>&lt;p&gt;At some point, every frontend developer realizes that it would be great to somehow verify that their code is working as expected. For me, this moment came after a painful migration from Vue 2 to Vue 3, with tons of bugs and defects. It left me with yet another strand of gray hair, and I wasn’t the only one suffering.&lt;/p&gt;
&lt;h3 id=&quot;my-project-and-technology-stack&quot;&gt;My project and technology stack&lt;/h3&gt;
&lt;p&gt;The current project I’m working on is a classic CRM, full of forms upon forms, list rendering, modals, and constant data transformation. The technology stack includes Vue 3, Pinia, Vite, and ElementPlus, which allows for fast and flexible interface development. &lt;/p&gt;
&lt;h3 id=&quot;why-unit-tests-&quot;&gt;Why unit tests?&lt;/h3&gt;
&lt;p&gt;After reading up on the theory behind pyramids and testing cups, I concluded that it’s easier to gradually add “unit” tests for new components and features, as well as cover any bugs with tests.&lt;/p&gt;
&lt;p&gt;Just a quick note on the testing pyramid. This concept, introduced by Martin Fowler, helps organize tests in a project. It involves three levels of testing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Unit Tests&lt;/strong&gt;: the most basic level of testing, which checks individual modules or functions. Unit tests are fast, easy to set up&lt;br&gt;
and ensure that components work correctly in isolation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Integration Tests&lt;/strong&gt;: test the interaction between multiple components or modules, like how components work together on a page.&lt;br&gt;
They are more complex and slower, but provide an understanding of the&lt;br&gt;
correctness of the integrations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;E2E tests (End-to-End Tests)&lt;/strong&gt;: test the entire application, simulating real user actions. E2E tests are slower and require&lt;br&gt;
complex infrastructure but ensure that the app works as a whole.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The word &quot;unit&quot; is in quotes here because, when testing components in a framework, there’s a nuance: testing components isn’t always about isolating them completely. &lt;/p&gt;
&lt;p&gt;Ideally, unit tests check isolated functions, (like the famous SUM () function), but in reality, we cannot entirely &quot;mock&quot; the framework. So, tests for Vue components are technically integration tests. However, for simplicity, the community continues to call them “unit tests” and I’ll stick with that terminology. &lt;/p&gt;
&lt;h3 id=&quot;testing-options-and-my-choice&quot;&gt;Testing options and my choice&lt;/h3&gt;
&lt;p&gt;Of course, there were other options. For example, I considered E2E testing with Playwright, which runs tests in real browsers simulating user actions. While Playwright is a powerful tool,I faced several infrastructure difficulties, which I’ll discuss later in the article.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Spoiler: the backend wasn't ready for this, and realistically, E2E tests should be written under the guidance of QA engineers - they are experts at breaking systems.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Ultimately, I decided to go with Vitest and Vue Test Utils as the main tools for testing. Vitest, as a test runner, was ideal, since Vite was already installed in the project, and Vue Test Utils provided all the necessary tools for mounting and modifying Vue components.&lt;/p&gt;
&lt;p&gt;Why Vue Test Utils and not Testing Library? First, Vue Text Urils is recommended by the Vue community (Testing Library was a bit late in supporting Vue 3). Second, I follow &quot;London School&quot; of testing, where we mock everything around our component to keep tests as fair isolated as possible. In addition, the Testing Library is built on top of VTU, and I wanted fewer dependencies.&lt;/p&gt;
&lt;h3 id=&quot;installing-and-setting-up&quot;&gt;Installing and setting up&lt;/h3&gt;
&lt;p&gt;To get started, we need to install and configure &lt;a rel=&quot;nofollow&quot; href=&quot;https://vitest.dev/guide/&quot;&gt;Vitest&lt;/a&gt; and &lt;a rel=&quot;nofollow&quot; href=&quot;https://test-utils.vuejs.org/installation/&quot;&gt;Vue test utils&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm i -D vitest @vue/test-utils
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since our tests will run in a Node.js environment, we need DOM implementers. Vitest recommends either happy-dom or jsdom. &lt;/p&gt;
&lt;p&gt;I chose jsdom because it’s more popular and faster, though happy-dom is an option as well as you can switch quickly.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm i jsdom -D
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;configuring-the-testing-environment&quot;&gt;Configuring the testing environment&lt;/h3&gt;
&lt;p&gt;Let's add the necessary configuration to vite.config.js.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;defineConfig({
...
test: {
environment: 'jsdom',
deps: {
  inline: ['element-plus'], // optional field
},
},
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since Pinia is in the stack, we can either use a real store or a &lt;a rel=&quot;nofollow&quot; href=&quot;https://pinia.vuejs.org/cookbook/testing.html&quot;&gt;test store for testing&lt;/a&gt;. I opted for a test store, as it’s easier to mutate directly during tests.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm i -D @pinia/testing
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;what-and-how-to-test-&quot;&gt;What and how to test?&lt;/h3&gt;
&lt;p&gt;Let's take a component with a button and a modal as an example.&lt;/p&gt;
&lt;p&gt;Component diagram and description &lt;/p&gt;
&lt;p&gt;Here’s the basic flow:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;There is a button (which is enabled if the user has permission).&lt;/li&gt;
&lt;li&gt;When clicked, it opens a form in a modal window (a separate&lt;br&gt;
component).&lt;/li&gt;
&lt;li&gt;When the form is filled and the &quot;Add&quot; button is clicked, a request is&lt;br&gt;
sent to the server.&lt;/li&gt;
&lt;li&gt;After a successful response, an event is emitted to indicate that the&lt;br&gt;
document has been added.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=10403058242445935004&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Component code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
 &amp;lt;div&amp;gt;
   &amp;lt;button :disabled=&quot;!isAddDocumentAvailable&quot; class=&quot;button button--primary&quot; @click=&quot;addIncomingMail&quot;&amp;gt;Add incoming correspondence&amp;lt;/button&amp;gt;
   &amp;lt;popup drawer :is-form-open=&quot;isFormOpen&quot; @closed=&quot;closeForm&quot;&amp;gt;
     &amp;lt;add-incoming-mail-modal :lawsuit-id=&quot;lawsuitId&quot; @closeForm=&quot;closeForm&quot; @mail-added=&quot;onMailChanged&quot; &amp;gt;
   &amp;lt;/popup&amp;gt;
 &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup &amp;gt;
import { ref } from 'vue';
import AddIncomingMailModal from '@/.../AddIncomingMailModal.vue';
const isFormOpen = ref(false);

const emits = defineEmits(['mail-added']);
defineProps({
 lawsuitId: {
   type: Number,
   required: true,
 },
});

function addIncomingMail() {
 isFormOpen.value = true;
}

function closeForm() {
 isFormOpen.value = false;
}
function onMailChanged() {
 emits('mail-added');
}
const isAddDocumentAvailable = computed(() =&amp;gt; {
 return checkAvailabilityByClaim(...);
});
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A bit of theory. &lt;/p&gt;
&lt;p&gt;To start, we need to decide what should be tested. The general advice is to follow the user's behavior and avoid focusing on implementation details, which makes sense. Essentially, we’ll treat the component as a black box: changing (preparing) its input data and testing the output.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Note: if the component has a complex function with heavy calculations, it should ideally be extracted and tested separately as a traditional unit test.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Mocks and stubs play a key role in isolating tests. Mocks replace real dependencies, while stubs simplify interaction with external systems (API, for example). The main rule is not to mock everything: only mock what affects the functionality being tested. This reduces test dependence on the mock.&lt;/p&gt;
&lt;p&gt;Input data includes properties, components, dependency injections, external stores, and slots. For a fair test, we should replace real components, stores, and DIs with stubs. This minimizes the risk of false positives.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=1106737940856126993&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Output: we should test the resulting HTMLcheck  API calls (which should also be stubbed to avoid affecting the backend), and verify that events are emitted and handled correctly outside the component. &lt;/p&gt;
&lt;p&gt;Let's move on to writing the test. First, we need to define what we are testing (as they say, &quot;draw a circle&quot;).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;describe('Adding incoming correspondence - AddIncomingMail', () =&amp;gt; {

 it.todo('The &quot;Add incoming correspondence&quot; button is active if the user has rights', async () =&amp;gt; {});

 it.todo('By clicking on the &quot;Add incoming correspondence&quot; button, a modal window for adding a new document opens', async () =&amp;gt; {});

 it.todo('When adding incoming correspondence, we generate an event outside the component', async () =&amp;gt; {});
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice how these test descriptions serve as self-documenting code — in theory.&lt;/p&gt;
&lt;p&gt;I prefer placing tests as close to the components as possible. This approach seems better suited for understanding how the component works.&lt;/p&gt;
&lt;p&gt;In general, we draw another circle, add details, and - we have a test.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { getButtonByText } from '@/mocks/helpersForTesting/searchElements/index.js';
import { mount } from '@vue/test-utils';
import AddIncomingMail from '@/components/AddIncomingMail.vue';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { createTestingPinia } from '@pinia/testing';

let wrapper; // storing a component instance

beforeEach(() =&amp;gt; { // before each test we mount the component with the initial settings
 wrapper = mount(AddIncomingMail, {
   global: {
plugins:[    
createTestingPinia({ // Create a test instance of Pinia
         createSpy: vi.fn,
         stubActions: false,
         initialState: {
           user: {
             state: {
               claims: [1,2],
             },
           },
         },
       }),]
     stubs: { // Since this is unit testing, we replace all child components with stubs
       AddIncomingMailModal: {
         name: 'AddIncomingMailModal',
         emits: ['mail-added'],
         template: '&amp;lt;div&amp;gt;&amp;lt;h2 &amp;gt;Add correspondence&amp;lt;/h2&amp;gt;&amp;lt;/div&amp;gt;',
       },
       popup: {
         props: { isFormOpen: false },
         template: '&amp;lt;div v-if=&quot;isFormOpen&quot;&amp;gt;&amp;lt;slot&amp;gt;&amp;lt;/div&amp;gt;',
       },
     },
   },
   props: {
     lawsuitId: 15,
   },
 });
});

afterEach(() =&amp;gt; { // After each test, we reset the stubs and destroy the instance
 wrapper.unmount();
 vi.resetAllMocks();
});

describe('Adding incoming correspondence - AddIncomingMail', () =&amp;gt; {
 it('The &quot;Add incoming correspondence&quot; button is active if the user has rights', async () =&amp;gt; {
   const button = getButtonByText({ wrapper, buttonText: 'Add incoming correspondence' }); // Made a helper for quick search of buttons

expect(button.element.disabled).toBe(false);
});

 it('By clicking on the &quot;Add incoming correspondence&quot; button, a modal window for adding a new document opens', async () =&amp;gt; {
   const button = getButtonByText({ wrapper, buttonText: 'Add incoming correspondence' });

   await button.trigger('click');
   await wrapper.vm.$nextTick(); //Since Vue makes changes asynchronously, it is important to wait

   expect(wrapper.text()).contain('Add correspondence');
 });

it('When adding incoming correspondence, we generate an event outside the component', async () =&amp;gt; {
   const button = getButtonByText({ wrapper, buttonText: 'Add incoming correspondence' });
   await button.trigger('click');
   await wrapper.vm.$nextTick();
   const modalStub = wrapper.findComponent({ name: 'AddIncomingMailModal' });

   modalStub.vm.$emit('mail-added');
   expect(wrapper.emitted()).toHaveProperty('mail-added');
 });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I would like to draw your attention to how element searching occurs. You should try to search for elements the way a user would search. A less recommended, but more popular approach is using data-testid=”foo”.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Note: Settings like the ones below:&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;afterEach(() =&amp;gt; { // After each test, we reset the stubs and destroy the instance
  wrapper.unmount();
  vi.resetAllMocks();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;can be moved to external files and connected in the vite.config.js settings as a global setting:&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;// vite.config.js
export default defineConfig({
  test: {
    // ...
    setupFiles: ['path to your file'],
    // ...
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;problem-with-unit-tests&quot;&gt;Problem with unit tests&lt;/h3&gt;
&lt;p&gt;Ultimately, unit tests help establish a contract between components. However, there's a downside: if we change the modal’s code, for example, its emitted events, the test for AddIncomingMail won’t fail. &lt;/p&gt;
&lt;p&gt;This is where integration tests (as mentioned earlier) come in, testing larger sections of the app. &lt;/p&gt;
&lt;p&gt;I haven’t fully implemented those tests yet, as I’m still thinking about the infrastructure and mocks (particularly for server requests), through a conditional &lt;a rel=&quot;nofollow&quot; href=&quot;https://mswjs.io/&quot;&gt;msw&lt;/a&gt;. However, there is a problem with maintaining the relevance of mocked responses (fixtures). Ideally, I'd like to use “Vitest browser mode” or “Playwright CT”, but these tools are still in their infancy. &lt;/p&gt;
&lt;p&gt;Start testing your code early and don’t treat testing as something separate from development. Yes, at first it will be tough, and yes, it will continue to be tough, but soon enough, you’ll just get used to it.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Author: Dmitry Simonov, Frontend Developer at ByteMinds&lt;/em&gt;&lt;/p&gt;
</description>
<category>Web Dev</category>
<guid isPermaLink="true">https://coderlegion.com/1682/how-i-started-writing-unit-tests-for-vue-components</guid>
<pubDate>Sun, 30 Mar 2025 02:37:53 +0000</pubDate>
</item>
</channel>
</rss>