<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Maya Shavin 🌷☕️🏡</title>
    <description>The latest articles on Forem by Maya Shavin 🌷☕️🏡 (@mayashavin).</description>
    <link>https://forem.com/mayashavin</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F146930%2Fd38cb498-9e87-434a-8510-0430a2c28600.jpeg</url>
      <title>Forem: Maya Shavin 🌷☕️🏡</title>
      <link>https://forem.com/mayashavin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/mayashavin"/>
    <language>en</language>
    <item>
      <title>What would I do without AI?</title>
      <dc:creator>Maya Shavin 🌷☕️🏡</dc:creator>
      <pubDate>Tue, 27 Jan 2026 07:39:07 +0000</pubDate>
      <link>https://forem.com/mayashavin/what-would-i-do-without-ai-51ik</link>
      <guid>https://forem.com/mayashavin/what-would-i-do-without-ai-51ik</guid>
      <description>&lt;p&gt;&lt;em&gt;That’s probably the most common sentence my colleagues and I say at work these days.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;AI didn’t arrive with a big announcement. It slowly crept into my daily engineering workflow—first as a coding assistant, then as a search tool, and eventually as something much closer to a thinking partner. Not a replacement. Not magic. And definitely not something I trust blindly.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I’m a lead engineer working in large, complex systems, where context, history, and tradeoffs matter just as much as writing code. In that environment, AI turned out to be most valuable not when it does the work for me, but when it helps me move faster through noise—finding information, understanding unfamiliar code, and turning rough ideas into something concrete.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post isn’t about hype, fear, or “AI will replace engineers.” It’s a practical look at how I actually use AI today: where it saves me hours, where it still gets things wrong, and why I see it less as a threat and more as a rescuer.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  AI as Your Trusted Slackbot
&lt;/h2&gt;

&lt;p&gt;I love Slack. Not because I work for Salesforce. I used Slack long before that. I’ve worked with Teams back in my Microsoft days, but Slack is on another level.&lt;/p&gt;

&lt;p&gt;And with recent Slackbot updates, Slack is no longer "communication" app. It’s a real assistant.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://slack.com/features/slackbot" rel="noopener noreferrer"&gt;Slackbot&lt;/a&gt; now searches across Slack, Confluence, Google Drive, Canvas, GUS (Salesforce’s Jira-wannabe), and more. It doesn’t just return links — it consolidates information and generates a structured answer based on what I ask.&lt;/p&gt;

&lt;p&gt;One recent example: my self-performance evaluation.&lt;/p&gt;

&lt;p&gt;I do keep a document where I track contributions and progress, but let’s be honest, no one captures everything. But what got captured, automaticall and silently, was my Slack activity: discussions, design reviews, investigations, decisions.&lt;/p&gt;

&lt;p&gt;So I asked Slackbot to summarize my contributions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Ff_auto%2Cq_auto%2Fv1675680195%2Farticles%2Fllm%2FScreenshot_2026-01-26_at_9.19.05" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Ff_auto%2Cq_auto%2Fv1675680195%2Farticles%2Fllm%2FScreenshot_2026-01-26_at_9.19.05" alt="Screenshot of Slackbot summarizing contributions" width="898" height="302"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And boom! Out came a detailed list of work, grouped by features and discussions, with a clear impact summary, references, and footnotes. My job after that was pretty straightforward: merge it with my own notes and pass it through another LLM agent to polish it according to the evaluation template.&lt;/p&gt;

&lt;p&gt;Is it perfect? No.&lt;/p&gt;

&lt;p&gt;It once pulled demo slides from a “Maya” who &lt;strong&gt;was… not me&lt;/strong&gt; (&lt;em&gt;turned out it was a made-up Maya as a demo case&lt;/em&gt; 😄). It still requires my review to avoid hallucinations or incorrect context.&lt;/p&gt;

&lt;p&gt;But saving hours of digging through old docs and Slack threads? That alone is a &lt;strong&gt;huge&lt;/strong&gt; win.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code Analyzer &amp;amp; Assistant
&lt;/h2&gt;

&lt;p&gt;Of course, we can’t talk about AI without talking about code.&lt;/p&gt;

&lt;p&gt;We’ve moved far beyond &lt;em&gt;copilot that writes a function&lt;/em&gt;. With tools like &lt;a href="https://cursor.com/" rel="noopener noreferrer"&gt;Cursor&lt;/a&gt;, &lt;a href="https://claude.com/product/claude-code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; and MCP-based agents, AI now helps me understand large codebases.&lt;/p&gt;

&lt;p&gt;Not vibe coding.&lt;/p&gt;

&lt;p&gt;Not replacing engineers.&lt;/p&gt;

&lt;p&gt;But acting as a powerful assistant.&lt;/p&gt;

&lt;p&gt;I can ask questions like:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“Who calls this function?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“Map the flow from this UI component to the backend.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“Which part of the code triggers this LLM orchestration?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Within minutes, it maps relationships across modules in a &lt;strong&gt;massive monolith&lt;/strong&gt; codebase and explains them in plain language—saving hours (or days) of digging through unfamiliar code. This is especially helpful when working in languages or systems that aren’t your home turf (hello, Java).&lt;/p&gt;

&lt;p&gt;I felt this most during a recent hackathon. Within 24 hours, our team reused existing internal UI and server features from multiple teams, layered our logic on top, and aimed to get as close to production-ready as possible. We also used AI as part of the product itself, helping customers reduce onboarding time from months to hours.&lt;/p&gt;

&lt;p&gt;The result?&lt;/p&gt;

&lt;p&gt;New technical knowledge unlocked, a working demo with real data, and a hackathon award.&lt;/p&gt;

&lt;h2&gt;
  
  
  Professional Content Editor for Professional Discussions
&lt;/h2&gt;

&lt;p&gt;One underrated use of AI: leveling up professional communication.&lt;/p&gt;

&lt;p&gt;Writing technical design docs, business justifications, RCA reports, or even a Slack announcement used to be hard, especially as non-native English speakers. Engineers aren’t trained writers or marketers, and it shows.&lt;/p&gt;

&lt;p&gt;With tools like ChatGPT or &lt;a href="https://notebooklm.google/" rel="noopener noreferrer"&gt;Gemini&lt;/a&gt;, I can now brainstorm ideas, structure my thoughts, draft proposals, get them refined, polished, and critiqued.&lt;/p&gt;

&lt;p&gt;The key is asking for criticism. If you don’t, AI will happily agree with everything you write.&lt;/p&gt;

&lt;p&gt;This isn’t limited to design docs. It’s just as useful for documentation, RCAs, or any message that goes beyond your immediate team.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Ff_auto%2Cq_auto%2Fv1675680195%2Farticles%2Fllm%2Fai_editor" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Ff_auto%2Cq_auto%2Fv1675680195%2Farticles%2Fllm%2Fai_editor" alt="Screenshot of a relaxing engineer sitting with an AI working" width="1536" height="1024"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And yes, Slack announcements too. Feed it your intent, and it’ll give you a version that sounds like you, just clearer and without grammar issues.&lt;/p&gt;

&lt;p&gt;I even built an RCA agent that generates detailed bug reports from investigations, Slack threads, and standard templates, ready for review and publishing. It also won a hackathon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pair Programmer That Turns Ideas Into Tools
&lt;/h2&gt;

&lt;p&gt;One of the biggest shifts for me is how AI helps turn ideas into production tools.&lt;/p&gt;

&lt;p&gt;Call it vibe coding if you want. But I use it to draft internal tools that boost productivity: setting up dev environments, provisioning mobile simulators, automating workflows with Python and Bash. Those things that used to take weeks of trial and error, now take a day or less, with fast feedback loops. From there, I refine and improve the solution myself.&lt;/p&gt;

&lt;p&gt;Since leaning into this, I’ve released several small tools that help my team move faster and make impact sooner. And it doesn't stop there.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Virtual Slack On-Call Engineer
&lt;/h2&gt;

&lt;p&gt;Working across large systems and multiple projects usually means monitoring countless Slack channels—supporting product managers, solution architects, customer support, and other engineers. Many questions are repetitive or already answered in documentation or past discussions, but it’s often faster for people to tag the on-call engineer than to search for them. For us as engineers, constantly switching contexts across channels is expensive and inefficient.&lt;/p&gt;

&lt;p&gt;By integrating AI agents into Slack, I can set up a virtual on-call engineer that monitors specific channels, is grounded in documentation, known issues, and discussion history, and continuously indexes new information. It answers common questions automatically and escalates only complex cases, reducing interruptions while still ensuring timely, accurate responses.&lt;/p&gt;

&lt;p&gt;With this &lt;a href="https://slack.com/blog/transformation/engineering-agent-slack-productivity" rel="noopener noreferrer"&gt;Engineer Agent&lt;/a&gt;, my team and I can focus on high-impact work without being bogged down by repetitive queries.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Ff_auto%2Cq_auto%2Fv1675680195%2Farticles%2Fllm%2FScreenshot_2026-01-26_at_9.04.43" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Ff_auto%2Cq_auto%2Fv1675680195%2Farticles%2Fllm%2FScreenshot_2026-01-26_at_9.04.43" alt="Screenshot of Engineering Agent in Slack with Agentforce" width="357" height="144"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Will AI replace my job one day?&lt;/p&gt;

&lt;p&gt;Maybe. Just like my role could be replaced by a younger engineer or by the industry evolving. No one is irreplaceable, especially at work. Even spaghetti code won’t save you forever.&lt;/p&gt;

&lt;p&gt;But should I worry? I don't know. What I do know is this: AI helps me onboard faster, cut through noise, and focus on what actually matters—building better products. When AI is wrong, it’s on me to notice. When it suggests a shortcut, it’s on me to decide if it’s the right one.&lt;/p&gt;

&lt;p&gt;I don’t see AI as a junior engineer or a looming threat. I see it as a companion—reducing friction, speeding things up, and helping me focus on what actually matters.&lt;/p&gt;

&lt;p&gt;As long as I stay in control of the decisions, that’s a tradeoff I’m happy to make.&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;If you’d like to continue the conversation, you can find me on &lt;a href="https://x.com/MayaShavin" rel="noopener noreferrer"&gt;X&lt;/a&gt; or &lt;a href="https://www.linkedin.com/in/mayashavin" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Found this post helpful? Give it a like or share it with someone who might need it 👇🏼&lt;/p&gt;

</description>
      <category>ai</category>
      <category>aitools</category>
      <category>engineering</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Making a custom input counter component accessible</title>
      <dc:creator>Maya Shavin 🌷☕️🏡</dc:creator>
      <pubDate>Wed, 16 Apr 2025 07:32:43 +0000</pubDate>
      <link>https://forem.com/mayashavin/making-a-custom-input-counter-component-accessible-1e3m</link>
      <guid>https://forem.com/mayashavin/making-a-custom-input-counter-component-accessible-1e3m</guid>
      <description>&lt;p&gt;&lt;em&gt;This blog post is not a tutorial on how to build an input counter component, but rather a discussion on how to structure the component for accessibility and good practice.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem of overlapping elements
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Cf_auto%2Fv1744754170%2Farticles%2FCSS%2FCounter" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Cf_auto%2Fv1744754170%2Farticles%2FCSS%2FCounter" alt="Counter component" width="398" height="210"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We recently encountered an input counter component that failed the accessibility test performed by &lt;a href="https://www.evinced.com/products/flow-analyzer-for-web" rel="noopener noreferrer"&gt;Evinced Web Flow Analyzer&lt;/a&gt;. The reported error was &lt;em&gt;"There is an overlapping between interactive elements"&lt;/em&gt; involving the increment &lt;code&gt;button&lt;/code&gt; and the &lt;code&gt;input&lt;/code&gt; field. This issue surprised us because, at first glance, the component appeared accessible, with keyboard navigation, voiceover, and focus state working as expected during manual testing.&lt;/p&gt;

&lt;p&gt;To investigate, we examined the component's implementation. Initially, its HTML template seemed well-structured, with the increment &lt;code&gt;button&lt;/code&gt;, &lt;code&gt;input&lt;/code&gt; field, and decrement &lt;code&gt;button&lt;/code&gt; nested as sibling elements within a &lt;code&gt;div&lt;/code&gt; container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"container"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"Increment value"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn incr"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;+&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"number"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"input"&lt;/span&gt; &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"Counter value"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"counter"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"Decrement value"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn decr"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;-&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, the component's CSS styling revealed a different story. The increment and decrement buttons were absolutely positioned, while the &lt;code&gt;input&lt;/code&gt; field was styled with &lt;code&gt;width: calc(100% - 60px)&lt;/code&gt; and horizontal padding (&lt;code&gt;padding-inline: 30px&lt;/code&gt;), aligning with the button widths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.container&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;/**... */&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;150px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;relative&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.btn&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;min-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.incr&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.decr&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.counter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;padding-inline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100%&lt;/span&gt; &lt;span class="n"&gt;-&lt;/span&gt; &lt;span class="m"&gt;60px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;text-align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While visually effective, this CSS introduced poor positioning practices:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Using &lt;code&gt;position: absolute&lt;/code&gt; removes buttons from the normal document flow, forcing the browser into additional layout computations.&lt;/li&gt;
&lt;li&gt;Button positions aren't relative to the &lt;code&gt;input&lt;/code&gt; field, causing additional complexity in CSS rules when adjusting font sizes, widths, or container dimensions.&lt;/li&gt;
&lt;li&gt;Absolute positioning complicates RTL/LTR language support (such as Arabic or Hebrew), requiring extra CSS adjustments.&lt;/li&gt;
&lt;li&gt;There is no actual necessity for absolute positioning since elements are already correctly structured in HTML.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Additionally, the &lt;code&gt;left: 1px&lt;/code&gt; rule caused a &lt;code&gt;1px&lt;/code&gt; overlap of the increment &lt;code&gt;button&lt;/code&gt; on the &lt;code&gt;input&lt;/code&gt; field, directly triggering the accessibility error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing overlapping and RTL issues using CSS Flexbox
&lt;/h2&gt;

&lt;p&gt;A simple fix would be adjusting &lt;code&gt;left: 1px&lt;/code&gt; to &lt;code&gt;left: 0&lt;/code&gt;, removing the immediate overlap. However, this would only patch the bug without addressing the root issue (&lt;code&gt;position: absolute&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;A better solution is using CSS Flexbox for the container, eliminating absolute positioning entirely. The updated CSS rules remove unnecessary properties (&lt;code&gt;left&lt;/code&gt;, &lt;code&gt;right&lt;/code&gt;, &lt;code&gt;height: 100%&lt;/code&gt;, &lt;code&gt;padding&lt;/code&gt;, and &lt;code&gt;calc()&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.container&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="n"&gt;lightgray&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.btn&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;min-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;text-align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, applying Flexbox introduces an overflow issue for the &lt;code&gt;input&lt;/code&gt; field, as form elements inherently have a default &lt;code&gt;width&lt;/code&gt; set by browser stylesheets.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Cf_auto%2Fv1744754170%2Farticles%2FCSS%2FCounter_Overflow" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Cf_auto%2Fv1744754170%2Farticles%2FCSS%2FCounter_Overflow" alt="Counter component with an overflowed input" width="508" height="220"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Resolving input overflow in Flexbox
&lt;/h3&gt;

&lt;p&gt;To address the overflow, set &lt;code&gt;min-width: 0&lt;/code&gt; and &lt;code&gt;flex: 1&lt;/code&gt; on the &lt;code&gt;input&lt;/code&gt; field. This ensures it dynamically shrinks or grows within the available container space:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;/** ... */&lt;/span&gt;
  &lt;span class="nl"&gt;min-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, using a &lt;code&gt;fieldset&lt;/code&gt; instead of a &lt;code&gt;div&lt;/code&gt; container could omit the &lt;code&gt;min-width: 0&lt;/code&gt; property. This provides a semantically meaningful solution beneficial for accessibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  Supporting RTL/LTR languages
&lt;/h3&gt;

&lt;p&gt;With Flexbox, the component inherently supports RTL/LTR languages without additional CSS rules. To verify this, use the &lt;code&gt;dir&lt;/code&gt; attribute on the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"container"&lt;/span&gt; &lt;span class="na"&gt;dir=&lt;/span&gt;&lt;span class="s"&gt;"rtl"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"increment"&lt;/span&gt; &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"Increment value"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn incr"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;+&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"number"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"input"&lt;/span&gt; &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"Counter value"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"counter"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"decrement"&lt;/span&gt; &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"Decrement value"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn decr"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;-&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach automatically swaps the increment and decrement buttons appropriately, maintaining a centered input field.&lt;/p&gt;


  





&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;In this blog post, we addressed accessibility issues caused by overlapping elements in an input counter component due to absolute positioning. By replacing this with CSS Flexbox and adjusting the CSS style of &lt;code&gt;input&lt;/code&gt; field, we significantly improved the component's accessibility and flexibility, providing RTL/LTR language support out of the box.&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;Learn about Vue 3 and TypeScript with my new book &lt;a href="https://www.oreilly.com/library/view/learning-vue/9781492098812/" rel="noopener noreferrer"&gt;Learning Vue&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;Follow me on &lt;a href="https://x.com/MayaShavin" rel="noopener noreferrer"&gt;X&lt;/a&gt; | &lt;a href="https://www.linkedin.com/in/mayashavin" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Like this post or find it helpful? Share it 👇🏼 😉&lt;/p&gt;

</description>
      <category>css</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>a11y</category>
    </item>
    <item>
      <title>Managing Multi-Step Forms in Vue with XState</title>
      <dc:creator>Maya Shavin 🌷☕️🏡</dc:creator>
      <pubDate>Wed, 12 Feb 2025 08:48:43 +0000</pubDate>
      <link>https://forem.com/mayashavin/managing-multi-step-forms-in-vue-with-xstate-5fhj</link>
      <guid>https://forem.com/mayashavin/managing-multi-step-forms-in-vue-with-xstate-5fhj</guid>
      <description>&lt;p&gt;&lt;em&gt;State management is essential for any application, ensuring a consistent data flow and predictable behavior. Choosing the right approach impacts scalability and maintainability.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;In this article, we'll explore how to refactor a multi-step sign-up form in Vue.js to use XState, a state management library based on finite state machines, making state handling more structured and efficient.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Table of Contents&lt;/li&gt;
&lt;li&gt;The challenge of managing state a multi-step sign-up form wizard&lt;/li&gt;
&lt;li&gt;What Are State Machines &amp;amp; XState?&lt;/li&gt;
&lt;li&gt;
Building a sign-up form wizard machine with XState

&lt;ul&gt;
&lt;li&gt;Defining the State Machine&lt;/li&gt;
&lt;li&gt;Integrating XState in the Component&lt;/li&gt;
&lt;li&gt;Adding Asynchronous Submission&lt;/li&gt;
&lt;li&gt;Updating the UI for Submission Status&lt;/li&gt;
&lt;li&gt;Passing Data to submitForm&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Mapping Component's Data to Context&lt;/li&gt;

&lt;li&gt;Resources&lt;/li&gt;

&lt;li&gt;

Summary

&lt;ul&gt;
&lt;li&gt;What's next?&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  The challenge of managing state a multi-step sign-up form wizard
&lt;/h2&gt;

&lt;p&gt;Let's say we have a sign-up form built as a two-step wizard: one step to collect the user's name and another for their email address. We'll call this component &lt;code&gt;SignupFormWizard&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;template&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;form&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-main-view"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"isNameStep"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Name&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Name"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;v-else-if=&lt;/span&gt;&lt;span class="s"&gt;"isEmailStep"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Email&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Email"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;v-else-if=&lt;/span&gt;&lt;span class="s"&gt;"isSubmitStep"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Submitting...&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"prev"&lt;/span&gt; &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"isEmailStep"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Prev&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"next"&lt;/span&gt; &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"isNameStep"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Next&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt; &lt;span class="na"&gt;v-else-if=&lt;/span&gt;&lt;span class="s"&gt;"isEmailStep"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Submit&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's how we manage the local state in the &lt;code&gt;script&lt;/code&gt; section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;setup&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reactive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;computed&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;reactive&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isNameStep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isEmailStep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isSubmitStep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;submit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our form behaves as follows:&lt;/p&gt;


  


&lt;p&gt;In this implementation, we track the form's step using a step variable with &lt;code&gt;ref()&lt;/code&gt;, store form data in a &lt;code&gt;reactive&lt;/code&gt; object, and define functions to handle navigation between steps and submission.&lt;/p&gt;

&lt;p&gt;For a simple form like this, this approach works fine. However, as the form grows in complexity, state management becomes more challenging. If we need to add more steps, handle asynchronous submission (including error and success states), or introduce additional logic, the code can quickly become difficult to maintain.&lt;/p&gt;

&lt;p&gt;So, how can we simplify this process while making it more predictable and scalable? Let's explore a better approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are State Machines &amp;amp; XState?
&lt;/h2&gt;

&lt;p&gt;State machines are models that define a system's behavior by breaking it down into a finite number of states. Transitions between these states occur one at a time, triggered by predefined events.&lt;/p&gt;

&lt;p&gt;In simple terms, states act like nodes in a graph, while events function as edges connecting these nodes. Every state and transition is explicitly defined.&lt;/p&gt;

&lt;p&gt;State machines are particularly useful for managing complex systems because they provide a structured and predictable way to handle application state. A classic example is a traffic light system, which consists of three main states: red, yellow, and green. The system follows a strict sequence—transitioning from &lt;code&gt;red&lt;/code&gt; to &lt;code&gt;yellow&lt;/code&gt;, then from &lt;code&gt;yellow&lt;/code&gt; to &lt;code&gt;green&lt;/code&gt;, and back—using a &lt;code&gt;next()&lt;/code&gt; event that can be scheduled.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Fxstate%2Ftraffic_lights.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Fxstate%2Ftraffic_lights.gif" alt="List of cards displayed in browser with minimum CSS" width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Building on this concept, &lt;a href="https://stately.ai/docs/quick-start" rel="noopener noreferrer"&gt;XState&lt;/a&gt; provides a declarative and predictable approach to state management in TypeScript. It can integrate with modern front-end frameworks like React and Vue through dedicated packages such as &lt;code&gt;@xstate/react&lt;/code&gt; and &lt;code&gt;@xstate/vue&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Next, let's explore how we can refactor our &lt;code&gt;SignupFormWizard&lt;/code&gt; component to use XState.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building a sign-up form wizard machine with XState
&lt;/h2&gt;

&lt;p&gt;To integrate XState, we install the necessary packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;xstate @xstate/vue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Defining the State Machine
&lt;/h3&gt;

&lt;p&gt;We create a new file, &lt;code&gt;machines/signUpMachine.js&lt;/code&gt;, and set up the state machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;setup&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;xstate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signUpMachineConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;signUpMachine&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signUpMachine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signUpMachineConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createMachine&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;initial&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
  &lt;span class="na"&gt;states&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
      &lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;NEXT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; 
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
      &lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;PREV&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;SUBMIT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;onsubmit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; 
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;onsubmit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This defines a three-state machine (&lt;code&gt;name&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, and &lt;code&gt;onsubmit&lt;/code&gt;) with transitions triggered by &lt;code&gt;NEXT&lt;/code&gt;, &lt;code&gt;PREV&lt;/code&gt;, and &lt;code&gt;SUBMIT&lt;/code&gt; events.&lt;/p&gt;

&lt;h3&gt;
  
  
  Integrating XState in the Component
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;SignupFormWizard.vue&lt;/code&gt;, we connect the machine using &lt;code&gt;useMachine()&lt;/code&gt; from &lt;code&gt;@xstate/vue&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;setup&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useMachine&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@xstate/vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;signUpMachine&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../machines/signUpMachine&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;send&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMachine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signUpMachine&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We then replace the local &lt;code&gt;step&lt;/code&gt; variable with &lt;code&gt;state.matches()&lt;/code&gt; for checking the active state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isNameStep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isEmailStep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isSubmitStep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;onsubmit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also refactor navigation methods to use &lt;code&gt;send()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PREV&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NEXT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;submit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SUBMIT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By doing so, we keeps the UI the same but makes state management more predictable.&lt;/p&gt;

&lt;p&gt;Next, let's add more features to our form machine, such as asynchronous submission.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding Asynchronous Submission
&lt;/h3&gt;

&lt;p&gt;To handle an asynchronous function and its states, we use &lt;code&gt;fromPromise&lt;/code&gt; helper as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;fromPromise&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;xstate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;submitForm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fromPromise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Handle submission logic&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;fromPromise()&lt;/code&gt; then creates an XState actor that triggers the &lt;code&gt;onDone&lt;/code&gt; event when the async function is resolved, and &lt;code&gt;onError&lt;/code&gt; event when rejected.&lt;/p&gt;

&lt;p&gt;We then modify the &lt;code&gt;onsubmit&lt;/code&gt; state to &lt;code&gt;invoke&lt;/code&gt; this function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signUpMachine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signUpMachineConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createMachine&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;//...&lt;/span&gt;
  &lt;span class="na"&gt;states&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;onsubmit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;submitForm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;onDone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;onError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;RESET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;RETRY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;onsubmit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this setup, we also add &lt;code&gt;RETRY&lt;/code&gt; and &lt;code&gt;RESET&lt;/code&gt; events to &lt;code&gt;error&lt;/code&gt; and &lt;code&gt;success&lt;/code&gt; states, respectively, to handle the retry and reset functionalities.&lt;/p&gt;

&lt;p&gt;Next, let's modify our component to reflect these changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Updating the UI for Submission Status
&lt;/h3&gt;

&lt;p&gt;We modify the &lt;code&gt;template&lt;/code&gt; to display &lt;code&gt;success&lt;/code&gt; and &lt;code&gt;error&lt;/code&gt; states:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;template&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;form&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-main-view"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!--...--&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;v-else-if=&lt;/span&gt;&lt;span class="s"&gt;"isSuccess"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Form submitted successfully!&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"reset"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Reset&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;v-else-if=&lt;/span&gt;&lt;span class="s"&gt;"isError"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Submission failed&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"retry"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Retry&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!--...--&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And we update the component logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isSuccess&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RESET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;retry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RETRY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With these changes, users can now retry or reset the form after submission.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1738789692%2Farticles%2Fxstate%2Fform_reset.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1738789692%2Farticles%2Fxstate%2Fform_reset.gif" alt="Flow of the form with successful submission" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Passing Data to submitForm
&lt;/h3&gt;

&lt;p&gt;To store and pass form data, update the machine’s &lt;code&gt;context&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signUpMachine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signupMachineConfigs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createMachine&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;//...&lt;/span&gt;
  &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;//...&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, we update the &lt;code&gt;SUBMIT&lt;/code&gt; event to perform side actions and update the context data with &lt;code&gt;assign()&lt;/code&gt; method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signUpMachine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signUpMachineConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createMachine&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;//...&lt;/span&gt;
  &lt;span class="na"&gt;states&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;//...&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;//...&lt;/span&gt;
        &lt;span class="na"&gt;SUBMIT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;onsubmit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
          &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;SignupFormWizard&lt;/code&gt;, we modify the &lt;code&gt;submit()&lt;/code&gt; function to include the form data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;submit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SUBMIT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To ensure the &lt;code&gt;submitForm&lt;/code&gt; function receives the required form data, we update the &lt;code&gt;onsubmit&lt;/code&gt; state to include an &lt;code&gt;input&lt;/code&gt; field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signUpMachine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signUpMachineConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createMachine&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;//...&lt;/span&gt;
  &lt;span class="na"&gt;states&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;//...&lt;/span&gt;
    &lt;span class="na"&gt;onsubmit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;submitForm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Xstate then injects then &lt;code&gt;input&lt;/code&gt; value into &lt;code&gt;submitForm&lt;/code&gt; function, as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;submitForm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fromPromise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="c1"&gt;//actual logic&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can also replace the local &lt;code&gt;formData&lt;/code&gt; state in the component with the machine's &lt;code&gt;context.formData&lt;/code&gt; instead. We do that next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mapping Component's Data to Context
&lt;/h2&gt;

&lt;p&gt;To bind form inputs to the machine's context, we can use &lt;code&gt;v-model&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Name"&lt;/span&gt; &lt;span class="na"&gt;v-model=&lt;/span&gt;&lt;span class="s"&gt;"state.context.formData.name"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Email"&lt;/span&gt; &lt;span class="na"&gt;v-model=&lt;/span&gt;&lt;span class="s"&gt;"state.context.formData.email"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, we can use an &lt;code&gt;UPDATE&lt;/code&gt; event to modify context dynamically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signUpMachine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signupMachineConfigs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createMachine&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;//...&lt;/span&gt;
  &lt;span class="na"&gt;states&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;NEXT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
              &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;}),&lt;/span&gt;
          &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;PREV&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
              &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;}),&lt;/span&gt;
          &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c1"&gt;//...&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And we bind inputs to this event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;update&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;UPDATE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;$event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And update the template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Name"&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;input=&lt;/span&gt;&lt;span class="s"&gt;"update"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Email"&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;input=&lt;/span&gt;&lt;span class="s"&gt;"update"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since &lt;code&gt;@input&lt;/code&gt; triggers on every keystroke, consider wrapping it with a debounce function for better performance.&lt;/p&gt;

&lt;p&gt;With this setup, our machine’s context stays in sync with the UI, eliminating the need to pass &lt;code&gt;formData&lt;/code&gt; in the &lt;code&gt;SUBMIT&lt;/code&gt; event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;submit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SUBMIT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And removing formData from the machine definition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signUpMachine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;signUpMachineConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createMachine&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;//...&lt;/span&gt;
  &lt;span class="na"&gt;states&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;//...&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
      &lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
        &lt;span class="c1"&gt;//...&lt;/span&gt;
        &lt;span class="na"&gt;SUBMIT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;onsubmit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; 
      &lt;span class="p"&gt;}&lt;/span&gt; 
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it! We've successfully refactored our multi-step form wizard to use XState for robust state management. Our state machine's flow now looks like this, with the initial state of &lt;code&gt;name&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Fxstate%2Fstate_machine_multi_form" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Fxstate%2Fstate_machine_multi_form" alt="State machine diagram for multi-step form" width="800" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Additionally, we can use XState’s &lt;a href="https://stately.ai/editor" rel="noopener noreferrer"&gt;visualizer tool&lt;/a&gt; to visualize our machine logic, by importing our code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://stately.ai/docs/quick-start" rel="noopener noreferrer"&gt;XState&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://stately.ai/docs/xstate-vue" rel="noopener noreferrer"&gt;XState Vue&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://stately.ai/editor" rel="noopener noreferrer"&gt;XState Visualizer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/mayashavin/multi-step-form-xstate-2025" rel="noopener noreferrer"&gt;Multi-step form wizard with Vue and XState&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;In this article, we explored how to manage a multi-step form in Vue.js using XState. We refactored our form to leverage state machines for predictable state management and introduced features like asynchronous submission and context-based data handling. This approach enhances maintainability and can be applied across different front-end frameworks.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's next?
&lt;/h3&gt;

&lt;p&gt;We can further improve our form by adding &lt;strong&gt;guards&lt;/strong&gt; to prevent invalid transitions and dynamically updating the UI based on conditions. Try experimenting with XState and see how it simplifies state management in your projects!&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;Learn about Vue 3 and TypeScript with my new book &lt;a href="https://www.oreilly.com/library/view/learning-vue/9781492098812/" rel="noopener noreferrer"&gt;Learning Vue&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;If you'd like to catch up with me sometimes, follow me on &lt;a href="https://x.com/MayaShavin" rel="noopener noreferrer"&gt;X&lt;/a&gt; | &lt;a href="https://www.linkedin.com/in/mayashavin" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Like this post or find it helpful? Share it 👇🏼 😉&lt;/p&gt;

</description>
      <category>xstate</category>
      <category>vue</category>
      <category>frontend</category>
      <category>javascript</category>
    </item>
    <item>
      <title>My Top 5 VSCode Extensions to Supercharge Your Markdown Writing</title>
      <dc:creator>Maya Shavin 🌷☕️🏡</dc:creator>
      <pubDate>Wed, 29 Jan 2025 07:08:40 +0000</pubDate>
      <link>https://forem.com/mayashavin/my-top-5-vscode-extensions-to-supercharge-your-markdown-writing-29c</link>
      <guid>https://forem.com/mayashavin/my-top-5-vscode-extensions-to-supercharge-your-markdown-writing-29c</guid>
      <description>&lt;p&gt;&lt;em&gt;As a developer and tech blogger, I frequently write technical blog posts and documentation in Markdown to share knowledge with the community. VSCode has been my go-to editor for managing and crafting Markdown content efficiently, besides coding. Over time, I've discovered some VSCode extensions that have transformed my writing experience and made it faster, and more productive. In this article, I'll share my top 5 VSCode extensions for Markdown writing and explain how they can supercharge your workflow.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Table of Contents&lt;/li&gt;
&lt;li&gt;Writing Markdown in VSCode&lt;/li&gt;
&lt;li&gt;Markdownlint by David Anson&lt;/li&gt;
&lt;li&gt;Markdown All in One by Yu Zhang&lt;/li&gt;
&lt;li&gt;Word Count by Microsoft&lt;/li&gt;
&lt;li&gt;:emojisense: by Matt Bierner&lt;/li&gt;
&lt;li&gt;CoCover - The AI Assistant for Generating Cover Images&lt;/li&gt;
&lt;li&gt;Summary&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Writing Markdown in VSCode
&lt;/h2&gt;

&lt;p&gt;Introduced in 2004, &lt;a href="https://www.markdownguide.org/getting-started/" rel="noopener noreferrer"&gt;Markdown is a lightweight markup language with plain text formatting syntax&lt;/a&gt;. It's widely used for drafting and writing documentation, such as README files. Personally, I find writing and storing my articles as Markdown files for my website a much more convenient approach, &lt;em&gt;especially in VSCode&lt;/em&gt; , compared to using standard WYSIWYG editors. It helps me stay focused and manage my content more efficiently.&lt;/p&gt;

&lt;p&gt;VSCode, a free and popular IDE (or code editor), provides excellent built-in support for Markdown editing, including syntax highlighting and preview features, such as a full preview with &lt;code&gt;Shift + Ctrl/Cmd + V&lt;/code&gt; or a side-by-side preview with &lt;code&gt;Ctrl/Cmd + K + V&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftools%2Fside_by_side_preview" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftools%2Fside_by_side_preview" alt="Markdown editor and preview side-by-side in VSCode" width="800" height="225"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Beyond these basic features, you can enhance your Markdown editing experience with extensions. Here are five VSCode extensions that have significantly improved my Markdown writing workflow:&lt;/p&gt;

&lt;h2&gt;
  
  
  Markdownlint by David Anson
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftools%2Fmarkdownlint" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftools%2Fmarkdownlint" alt="Markdownlint Extension in Marketplace" width="800" height="214"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=DavidAnson.vscode-markdownlint" rel="noopener noreferrer"&gt;Markdownlint&lt;/a&gt; is the first extension I installed when working with Markdown files. It keeps my Markdown clean and consistent by flagging common syntax issues such as missing spaces, trailing spaces, and inconsistent indentation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftools%2Flinting_preview" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftools%2Flinting_preview" alt="Inline error displayed when there is markdown violation" width="800" height="98"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Markdown All in One by Yu Zhang
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftools%2Fmarkdownallinone" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftools%2Fmarkdownallinone" alt="Markdownlint All in One in Marketplace" width="800" height="153"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=yzhang.markdown-all-in-one" rel="noopener noreferrer"&gt;Markdown All in One&lt;/a&gt; is a powerful extension that offers a collection of features to streamline Markdown editing. It includes shortcuts for common syntax elements like headers, lists, and tables, along with keyboard shortcuts for toggling bold, italics, and code formatting.&lt;/p&gt;

&lt;p&gt;One of my favorite features is its Table of Contents (TOC) generator, which automatically updates the TOC based on the headings in your file. This is particularly useful in helping readers navigate lengthy documents.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftools%2Ftoc_generate" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftools%2Ftoc_generate" alt="Table of Contents generated by Markdown All in One" width="800" height="263"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It also offers list-editing features for reorganizing content quickly, as well as handy tools like toggling code spans and automatic table formatting. These features make long-form document editing much more efficient and enjoyable.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fvideo%2Fupload%2Fv1737673973%2Farticles%2Ftools%2Flist_editing.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fvideo%2Fupload%2Fv1737673973%2Farticles%2Ftools%2Flist_editing.gif" alt="List editing in Markdown All in One" width="1030" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Word Count by Microsoft
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftools%2Fwordcount" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftools%2Fwordcount" alt="Word Count in Marketplace" width="800" height="156"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.wordcount" rel="noopener noreferrer"&gt;Word Count&lt;/a&gt; is a simple yet essential extension that shows the total word count of your current document in the status bar. It's perfect for tracking article length and ensuring your posts meet the word count requirements.&lt;/p&gt;

&lt;p&gt;Once installed, you'll find the word count displayed in the bottom status bar, updating in real-time as you type.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftools%2Fwordcount_statusbar" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftools%2Fwordcount_statusbar" alt="Word Count displayed in the status bar" width="800" height="90"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  :emojisense: by Matt Bierner
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftools%2Femojisense" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftools%2Femojisense" alt="Emojisense in Marketplace" width="800" height="229"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=bierner.emojisense" rel="noopener noreferrer"&gt;:emojisense:&lt;/a&gt; is a fun and practical extension for adding emojis to your Markdown files. It provides an inline, searchable emoji picker that makes finding and inserting emojis super easy.&lt;/p&gt;

&lt;p&gt;To use the extension, we type &lt;code&gt;:&lt;/code&gt; followed by the emoji name or keyword. The extension will display matching emojis for quick insertion. 💡&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fvideo%2Fupload%2Fv1737673973%2Farticles%2Ftools%2Femoji_editing.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fvideo%2Fupload%2Fv1737673973%2Farticles%2Ftools%2Femoji_editing.gif" alt="Emojisense in action" width="800" height="335"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  CoCover - The AI Assistant for Generating Cover Images
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftools%2Fcocover" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftools%2Fcocover" alt="CoCover in Marketplace" width="800" height="154"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Last but not least, &lt;a href="https://marketplace.visualstudio.com/items?itemName=MayaShavinStudio.cocover" rel="noopener noreferrer"&gt;CoCover&lt;/a&gt; is an AI-powered extension I developed for generating beautiful cover images directly in VSCode. It uses GitHub Copilot and OpenAI's DALL-E to create covers based on your article's title and content.&lt;/p&gt;


  


&lt;p&gt;With CoCover, you can generate blog covers, upload them to Cloudinary, or save them locally—all without leaving VSCode. It's a fantastic tool for streamlining your content creation workflow while making your articles visually appealing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;These are my top five VSCode extensions for Markdown writing. They've helped me streamline my workflow, minimize context switching, and create better Markdown-based content.&lt;/p&gt;

&lt;p&gt;What about you? What are your favorite VSCode extensions for Markdown writing? I'd love to hear your recommendations and suggestions! 😄&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;Learn about Vue 3 and TypeScript with my new book &lt;a href="https://www.oreilly.com/library/view/learning-vue/9781492098812/" rel="noopener noreferrer"&gt;Learning Vue&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;If you'd like to catch up with me sometimes, follow me on &lt;a href="https://x.com/MayaShavin" rel="noopener noreferrer"&gt;X&lt;/a&gt; | &lt;a href="https://www.linkedin.com/in/mayashavin" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Like this post or find it helpful? Share it 👇🏼 😉&lt;/p&gt;

</description>
      <category>markdown</category>
      <category>extensions</category>
      <category>vscode</category>
      <category>writing</category>
    </item>
    <item>
      <title>Mastering Flexible Layouts: CSS Flexbox VS Grid for Responsive Design</title>
      <dc:creator>Maya Shavin 🌷☕️🏡</dc:creator>
      <pubDate>Wed, 22 Jan 2025 08:21:13 +0000</pubDate>
      <link>https://forem.com/mayashavin/mastering-flexible-layouts-css-flexbox-vs-grid-for-responsive-design-46m3</link>
      <guid>https://forem.com/mayashavin/mastering-flexible-layouts-css-flexbox-vs-grid-for-responsive-design-46m3</guid>
      <description>&lt;p&gt;&lt;em&gt;In this post, we will discover different approaches to distribute a list of cards evenly, horizontally and responsively in different screen sizes using CSS Flex and Grid.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Table of Contents&lt;/li&gt;
&lt;li&gt;The Challenge&lt;/li&gt;
&lt;li&gt;Using CSS Flexbox to Create a Flexible List of Cards&lt;/li&gt;
&lt;li&gt;Displaying Cards Evenly with flex-grow and flex-basis&lt;/li&gt;
&lt;li&gt;Using CSS Grid for a Flexible List of Cards&lt;/li&gt;
&lt;li&gt;Summary&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Challenge
&lt;/h2&gt;

&lt;p&gt;In a gallery (or list) component, we often want to display items as cards, such as articles, products, or images. The number of cards per row can be flexible, depending on the container's width. Each card should expand to fill the available space and shrink to its minimum width as needed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Fcss%2Fdesign_sketch" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Fcss%2Fdesign_sketch" alt="Design of list card in two different width sizes" width="800" height="316"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The layout should remain consistent across screen sizes, with the cards distributed from left to right, wrapping to the next row as necessary. The cards should maintain equal height, width, and spacing between them.&lt;/p&gt;

&lt;p&gt;Here’s a simple starting point for the HTML structure and CSS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"list-items"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"item"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; &amp;gt;
    &lt;span class="c"&gt;&amp;lt;!--item's content--&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!--more items--&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
  &lt;span class="nc"&gt;.list-items&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;list-style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;500px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nc"&gt;.item&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="no"&gt;gray&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the above code, the browser displays the container according to the screen’s width but does not exceed 500px:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Fcss%2Fcss_grid_initial_list" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Fcss%2Fcss_grid_initial_list" alt="List of cards displayed in browser with minimum CSS" width="545" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Currently, the items are not distributed horizontally. We’ll fix this next using CSS Flexbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using CSS Flexbox to Create a Flexible List of Cards
&lt;/h2&gt;

&lt;p&gt;CSS Flexbox provides a flexible and responsive layout structure using the &lt;code&gt;display: flex&lt;/code&gt; property. To achieve horizontal flow, we use the &lt;code&gt;flex-wrap&lt;/code&gt; property to wrap cards to the next row when necessary, and &lt;code&gt;gap&lt;/code&gt; to define spacing between cards:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.list-items&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;flex-wrap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;wrap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, the cards flow horizontally:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Fcss%2Fcss_flex_list" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Fcss%2Fcss_flex_list" alt="A list of horizontal distributed cards using CSS Flex" width="800" height="221"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, not all cards have the same width (e.g., Card 9 and Card 10). To fix this, we can set a fixed &lt;code&gt;width&lt;/code&gt; for &lt;code&gt;.item&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.item&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;/* other styles */&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures all cards have the same width. But when the container's width changes, the cards cannot expand to fill the space. This leaves gaps at the end of each row, as shown below:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Fcss%2Fcss_flex_fixed_width_border" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Fcss%2Fcss_flex_fixed_width_border" alt="Cards distributed to the left and left some space each row on the right" width="800" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can try to address this using &lt;code&gt;justify-content&lt;/code&gt; properties like &lt;code&gt;space-between&lt;/code&gt;, &lt;code&gt;space-around&lt;/code&gt;, &lt;code&gt;space-evenly&lt;/code&gt;, or &lt;code&gt;center&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Fcss%2Fjustify_content_4" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Fcss%2Fjustify_content_4" alt="Different results in different value of justify content" width="800" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While these approaches work differently, none fully meet our goal of evenly distributing cards in size and spacing.&lt;/p&gt;

&lt;p&gt;Instead, we can use &lt;code&gt;flex-grow&lt;/code&gt; and &lt;code&gt;flex-basis&lt;/code&gt;, which we’ll explore next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Displaying Cards Evenly with flex-grow and flex-basis
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;flex-basis&lt;/code&gt; property defines the &lt;strong&gt;initial&lt;/strong&gt; size of a card before any extra space is distributed, while &lt;code&gt;flex-grow: 1&lt;/code&gt; (or simply &lt;code&gt;flex: 1&lt;/code&gt;) allows the card to grow relative to its siblings. Combining these properties ensures that cards distribute evenly in size and spacing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.item&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;/* other styles */&lt;/span&gt;
  &lt;span class="nl"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;flex-basis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here’s the result:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Fcss%2Fflex_basic" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Fcss%2Fflex_basic" alt="Create App button in LinkedIn Developer dashboard" width="800" height="349"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, the last card still expands to fill the remaining space because it has no sibling to share it with. This breaks our consistency goal. To address this issue, let's turn to CSS Grid.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using CSS Grid for a Flexible List of Cards
&lt;/h2&gt;

&lt;p&gt;CSS Grid is a 2-dimensional layout system that organizes content into rows and columns. To create a flexible card layout, we can use the &lt;code&gt;display: grid&lt;/code&gt; property:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.list-items&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While CSS Grid supports many properties similar to Flexbox, such as &lt;code&gt;gap&lt;/code&gt; and &lt;code&gt;alignment&lt;/code&gt; options, it requires us to define the layout structure in advance. For a flexible layout, we can use &lt;code&gt;grid-template-columns&lt;/code&gt; with &lt;code&gt;auto-fit&lt;/code&gt;, &lt;code&gt;minmax()&lt;/code&gt;, and &lt;code&gt;repeat()&lt;/code&gt; functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.list-items&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;/* other styles */&lt;/span&gt;
  &lt;span class="py"&gt;grid-template-columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;auto-fit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;minmax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100px&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here’s what the above code does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;repeat(&amp;lt;number&amp;gt;, &amp;lt;size&amp;gt;)&lt;/code&gt;: Repeats columns of a specified size. Using &lt;code&gt;auto-fit&lt;/code&gt; automatically adjusts the number of columns to fit the available space.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;minmax(&amp;lt;min&amp;gt;, &amp;lt;max&amp;gt;)&lt;/code&gt;: Defines the size range for each column, setting a &lt;strong&gt;minimum&lt;/strong&gt; size of &lt;code&gt;100px&lt;/code&gt; and a &lt;strong&gt;maximum&lt;/strong&gt; size of &lt;code&gt;1fr&lt;/code&gt; (1 fraction of the available space).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Combining these with &lt;code&gt;grid-template-columns&lt;/code&gt; ensures the layout adjusts responsively.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And that's it! The result is a perfectly responsive layout:&lt;/p&gt;


  


&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;CSS Flexbox and CSS Grid are powerful tools for creating flexible, responsive designs. While Flexbox excels at managing one-dimensional layouts, CSS Grid offers unparalleled flexibility for two-dimensional layouts. Understanding the strengths of each approach ensures you can choose the right tool for your design goals. I hope this article provides clarity and inspires you to create your next layout. If you have questions or feedback, feel free to reach out. &lt;/p&gt;

&lt;p&gt;Happy coding! 🚀&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;Learn about Vue 3 and TypeScript with my new book &lt;a href="https://www.oreilly.com/library/view/learning-vue/9781492098812/" rel="noopener noreferrer"&gt;Learning Vue&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;If you'd like to catch up with me sometimes, follow me on &lt;a href="https://x.com/MayaShavin" rel="noopener noreferrer"&gt;X&lt;/a&gt; | &lt;a href="https://www.linkedin.com/in/mayashavin" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Like this post or find it helpful? Share it 👇🏼 😉&lt;/p&gt;

</description>
      <category>cssgrid</category>
      <category>css</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Resolving Auto-Scroll issues for overflow container in a Nuxt app</title>
      <dc:creator>Maya Shavin 🌷☕️🏡</dc:creator>
      <pubDate>Wed, 15 Jan 2025 08:47:00 +0000</pubDate>
      <link>https://forem.com/mayashavin/resolving-auto-scroll-issues-for-overflow-container-in-a-nuxt-app-ikn</link>
      <guid>https://forem.com/mayashavin/resolving-auto-scroll-issues-for-overflow-container-in-a-nuxt-app-ikn</guid>
      <description>&lt;p&gt;&lt;em&gt;In this article, I share how I resolved the auto-scroll issue caused by an overflow container within a non-scrollable &lt;code&gt;body&lt;/code&gt; in a Nuxt app, and improved the user experience when scrolling my website&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Table of Contents&lt;/li&gt;
&lt;li&gt;The Initial Design&lt;/li&gt;
&lt;li&gt;The Issue&lt;/li&gt;
&lt;li&gt;The Solution&lt;/li&gt;
&lt;li&gt;Summary&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Initial Design
&lt;/h2&gt;

&lt;p&gt;When I built my website using Nuxt.js, my initial design was to have only the &lt;code&gt;main&lt;/code&gt; content container scrollable, while the &lt;code&gt;header&lt;/code&gt; and &lt;code&gt;footer&lt;/code&gt; remained fixed—without using CSS &lt;code&gt;fixed&lt;/code&gt; or &lt;code&gt;absolute&lt;/code&gt; positioning.&lt;/p&gt;





&lt;p&gt;To achieve this, I used a combination of CSS &lt;code&gt;flex&lt;/code&gt; and &lt;code&gt;overflow&lt;/code&gt; properties, starting with the &lt;code&gt;body&lt;/code&gt; tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the &lt;code&gt;default.vue&lt;/code&gt; layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"h-full flex flex-col bg-mayash-main-light dark-mode"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;nav-header&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;main&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex-1 overflow-y-auto overflow-x-hidden"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;slot&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/main&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;nav-footer&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the above code, I ensured the body was not scrollable using &lt;code&gt;overflow-hidden&lt;/code&gt; while keeping the height statically set to &lt;code&gt;100%&lt;/code&gt;. The &lt;code&gt;main&lt;/code&gt; container was set to &lt;code&gt;flex-1&lt;/code&gt; to expand and fill the remaining space after the header and footer. I used &lt;code&gt;overflow-y-auto&lt;/code&gt; to make the container scrollable vertically only. This setup allowed the page to work as intended based on the initial design.&lt;/p&gt;

&lt;p&gt;However, problems arose when I added the table of contents to the article page. The table of contents is a list of links to section headings within an article, and clicking on a link should scroll to the corresponding section. Unfortunately, it didn’t.&lt;/p&gt;





&lt;p&gt;Additionally, when refreshing the article page with an HTML anchor (&lt;code&gt;#&lt;/code&gt;) in the URL, the browser didn’t scroll to the desired section. Navigating back to the article page from another page also failed to auto-scroll to the top of the page. For example, navigating to the &lt;strong&gt;Speaking&lt;/strong&gt; page from the &lt;strong&gt;Article&lt;/strong&gt; page left the user at the bottom instead of at the top.&lt;/p&gt;





&lt;p&gt;Furthermore, I received complaints about the footer being &lt;em&gt;fixed&lt;/em&gt; at the bottom of the page, which some users found distracting when reading the article.&lt;/p&gt;

&lt;p&gt;Clearly, I needed to change my design and fix these issues. But is it that simple? Let's find out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Issue
&lt;/h2&gt;

&lt;p&gt;Nuxt.js 3 uses Vue Router to handle application routing, which includes auto-scrolling when navigating between pages, with a smooth transition effect. So, it should work, right?&lt;/p&gt;

&lt;p&gt;Unfortunately, it didn’t.&lt;/p&gt;

&lt;p&gt;I tried different workarounds, including &lt;a href="https://router.vuejs.org/guide/advanced/scroll-behavior" rel="noopener noreferrer"&gt;using Vue Router custom scroll behavior&lt;/a&gt;, &lt;a href="https://nuxt.com/docs/guide/recipes/custom-routing#using-approuteroptions" rel="noopener noreferrer"&gt;using &lt;code&gt;scrollBehaviorType&lt;/code&gt;&lt;/a&gt;, and even &lt;code&gt;window.scrollTo&lt;/code&gt;. None of these approaches worked.&lt;/p&gt;

&lt;p&gt;So, is there a way to fix this issue?&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;p&gt;After some research, I discovered that the issue was caused by the combination of the &lt;code&gt;overflow&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; CSS properties. When &lt;code&gt;height&lt;/code&gt; is set to a fixed value (like &lt;code&gt;100%&lt;/code&gt;) and &lt;code&gt;overflow&lt;/code&gt; on the &lt;code&gt;body&lt;/code&gt; tag is set to &lt;code&gt;hidden&lt;/code&gt;, the scrollbar displayed on the right side of the page isn’t for the &lt;code&gt;body&lt;/code&gt; tag but for the &lt;code&gt;main&lt;/code&gt; container instead.&lt;/p&gt;

&lt;p&gt;When navigating between routes or sections, the browser, by default, tries to scroll the &lt;code&gt;body&lt;/code&gt; tag. Since the &lt;code&gt;body&lt;/code&gt; is not scrollable, the browser doesn’t know which container to scroll, leading to the issue.&lt;/p&gt;

&lt;p&gt;There are several ways to work around this problem, such as querying the &lt;code&gt;main&lt;/code&gt; container's DOM reference and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTo" rel="noopener noreferrer"&gt;using the &lt;code&gt;scrollTo&lt;/code&gt; method&lt;/a&gt; to manually scroll the container to the desired section whenever the route changes. However, this approach is not ideal — it’s complex to implement and not a good practice.&lt;/p&gt;

&lt;p&gt;A more straightforward solution is to remove the &lt;code&gt;height: 100%&lt;/code&gt; and &lt;code&gt;overflow: hidden&lt;/code&gt; properties from the &lt;code&gt;body&lt;/code&gt; tag and use &lt;code&gt;position: sticky&lt;/code&gt; for the &lt;code&gt;header&lt;/code&gt; to keep it fixed at the top instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;/* overflow: hidden; */&lt;/span&gt;
  &lt;span class="c"&gt;/* height: 100%; */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nt"&gt;header&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sticky&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;z-index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also need to set &lt;code&gt;top: 0&lt;/code&gt; and &lt;code&gt;z-index&lt;/code&gt; to ensure the header remains at the top of the page and appears above other elements.&lt;/p&gt;

&lt;p&gt;And that’s it! The page now has the header fixed at the top, and the content automatically scrolls based on route changes or HTML anchor links, utilizing the browser’s default smooth transition effect.&lt;/p&gt;








&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;In this article, I shared how I resolved the auto-scroll issue caused by an overflow container within a non-scrollable &lt;code&gt;body&lt;/code&gt; in a Nuxt app. The issue stemmed from the combination of &lt;code&gt;overflow&lt;/code&gt; and a fixed &lt;code&gt;height&lt;/code&gt; CSS property and can affect any web project, not just those using Nuxt or Vue Router. Depending on your goals, the solution may involve simply removing this CSS combination or implementing a more complex workaround, such as manually triggering scrollTo on the target scrollable container. So, the next time you encounter this issue, you’ll know how to fix it 😉!&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;Learn about Vue 3 and TypeScript with my new book &lt;a href="https://www.oreilly.com/library/view/learning-vue/9781492098812/" rel="noopener noreferrer"&gt;Learning Vue&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;If you'd like to catch up with me sometimes, follow me on &lt;a href="https://x.com/MayaShavin" rel="noopener noreferrer"&gt;X&lt;/a&gt; | &lt;a href="https://www.linkedin.com/in/mayashavin" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Like this post or find it helpful? Share it 👇🏼 😉&lt;/p&gt;

</description>
      <category>nuxt</category>
      <category>css</category>
      <category>vue</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building Social Media Automation: LinkedIn Sharing with Serverless Function</title>
      <dc:creator>Maya Shavin 🌷☕️🏡</dc:creator>
      <pubDate>Wed, 08 Jan 2025 06:32:32 +0000</pubDate>
      <link>https://forem.com/mayashavin/building-social-media-automation-linkedin-sharing-with-serverless-function-47j5</link>
      <guid>https://forem.com/mayashavin/building-social-media-automation-linkedin-sharing-with-serverless-function-47j5</guid>
      <description>&lt;p&gt;&lt;em&gt;After publishing a new article or blog post, the need to promote it on social media arises. Manually sharing the post can be time-consuming and inefficient. In this article, we will explore how to build a serverless function to share an article URL on LinkedIn using its JavaScript API client and Netlify serverless functions. This is part of building an automated workflow for social media promotion.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Table of Contents&lt;/li&gt;
&lt;li&gt;Prerequisites&lt;/li&gt;
&lt;li&gt;
Getting started

&lt;ul&gt;
&lt;li&gt;Setting up the permissions&lt;/li&gt;
&lt;li&gt;Configuring OAuth 2.0 settings&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

Sharing a post with URL using LinkedIn API Client

&lt;ul&gt;
&lt;li&gt;Getting the user's unique id&lt;/li&gt;
&lt;li&gt;Sharing a post URL&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Exposing as a Netlify serverless function&lt;/li&gt;

&lt;li&gt;Testing the Functionality&lt;/li&gt;

&lt;li&gt;Deploying with Netlify&lt;/li&gt;

&lt;li&gt;Summary&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;To follow along with this tutorial, you will need the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A LinkedIn account&lt;/li&gt;
&lt;li&gt;Node.js and &lt;a href="https://docs.netlify.com/cli/get-started/" rel="noopener noreferrer"&gt;Netlify CLI&lt;/a&gt; installed.&lt;/li&gt;
&lt;li&gt;A Netlify account and a site created for deploying the serverless function.&lt;/li&gt;
&lt;li&gt;Basic knowledge of JavaScript and TypeScript.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;To start working with LinkedIn APIs, we need to perform the following steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Head to &lt;a href="https://www.linkedin.com/developers/" rel="noopener noreferrer"&gt;LinkedIn Developer Console&lt;/a&gt; with your LinkedIn account.&lt;/li&gt;
&lt;li&gt;Create a new app by clicking on the &lt;code&gt;Create App&lt;/code&gt; button. 
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Flinkedin%2Fcreate_new_app" alt="Create App button in LinkedIn Developer dashboard" width="800" height="86"&gt;
&lt;/li&gt;
&lt;li&gt;Fill in the details such as App Name (&lt;em&gt;Social Media Tester&lt;/em&gt;, for example) and App logo image.&lt;/li&gt;
&lt;li&gt;You will need to a LinkedIn Company page to associate with the app you are creating (any page you have admin access to be able to verify the connection afterwards).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Flinkedin%2Fcreate_app" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Flinkedin%2Fcreate_app" alt="The form of filling new app details" width="800" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once done, the portal will redirect you to the app dashboard, where we can start configuring the permissions and API products we need for the app.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Flinkedin%2Fapp_created" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Flinkedin%2Fapp_created" alt="App dashboard after created" width="754" height="258"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting up the permissions
&lt;/h3&gt;

&lt;p&gt;In the app dashboard, click on &lt;strong&gt;Products&lt;/strong&gt; tabs and request access to the &lt;code&gt;Share on LinkedIn&lt;/code&gt; and &lt;code&gt;Sign In with LinkedIn using OpenID Connect&lt;/code&gt; products.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Flinkedin%2Flinkedin_products" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Flinkedin%2Flinkedin_products" alt="Request access to Share on LinkedIn and Sign In with LinkedIn using OpenID Connect" width="682" height="258"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuring OAuth 2.0 settings
&lt;/h3&gt;

&lt;p&gt;With these permissions granted, we can head to &lt;a href="https://www.linkedin.com/developers/tools/oauth/token-generator" rel="noopener noreferrer"&gt;the OAuth 2.0 token generator tool&lt;/a&gt; to generate an access token for the app. The token should include the following scopes: &lt;code&gt;w_member_social&lt;/code&gt; for posting on behalf of the user, and &lt;code&gt;profile&lt;/code&gt; and &lt;code&gt;openid&lt;/code&gt; for user authentication and profile information.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Flinkedin%2Ftoken_scope" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Flinkedin%2Ftoken_scope" alt="Select the scopes available for the token" width="569" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This access token is a 3-legged OAuth token, ensuring that the user has explicitly authorized the application to act on their behalf. After generating the token, we can use it to authenticate and securely make requests to the LinkedIn APIs. Additionally, we can review the permissions and scopes granted to the app in the &lt;strong&gt;Auth&lt;/strong&gt; tab of the app dashboard.&lt;/p&gt;

&lt;p&gt;Great! Now that we have the access token and the app set up, we can start building the automation to post on LinkedIn on behalf of the user (which, in this case, is us).&lt;/p&gt;

&lt;h2&gt;
  
  
  Sharing a post with URL using LinkedIn API Client
&lt;/h2&gt;

&lt;p&gt;To start sharing posts programmatically, we can use &lt;a href="https://github.com/linkedin-developers/linkedin-api-client?tab=readme-ov-file#linkedin-api-javascript-client" rel="noopener noreferrer"&gt;the official LinkedIn API JavaScript Client for Node.js&lt;/a&gt; by installing it as a project dependency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;linkedin-api-client

&lt;span class="c"&gt;# or with yarn&lt;/span&gt;
yarn add linkedin-api-client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This library provides a straightforward and lightweight way to interact with LinkedIn API endpoints, leveraging Axios and TypeScript under the hood.&lt;/p&gt;

&lt;p&gt;Next, let's create a new file, &lt;code&gt;linkedin.ts&lt;/code&gt;, to encapsulate the logic for sharing posts on LinkedIn. We start by initializing a client instance to interact with the API, as shown below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// linkedin.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;RestliClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;linkedin-api-client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RestliClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Getting the user's unique id
&lt;/h3&gt;

&lt;p&gt;To post on behalf of a user, we first need to retrieve the user's unique ID (which is different from the user's LinkedIn handle). This can be done by using the &lt;code&gt;/userinfo&lt;/code&gt; endpoint with the access token generated earlier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// linkedin.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getUserId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;resourcePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/userinfo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;accessToken&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;userResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The unique ID is located in the &lt;code&gt;sub&lt;/code&gt; field of the response's &lt;code&gt;data&lt;/code&gt;. This value is required for the next step: sharing a post on the user's behalf.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sharing a post URL
&lt;/h3&gt;

&lt;p&gt;Within &lt;code&gt;linkedin.ts&lt;/code&gt;, we define a function to share a post's URL as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SharePostArgs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sharePost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SharePostArgs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;//logic&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;sharePost&lt;/code&gt; function takes the access token and the content to share, which includes the URL and the text to accompany the post. We will then &lt;code&gt;create&lt;/code&gt; a new post &lt;code&gt;entity&lt;/code&gt; on the User Generated Contents resource using &lt;code&gt;/ugcPosts&lt;/code&gt; endpoint, as shown below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sharePost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SharePostArgs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;resourcePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/ugcPosts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;//entity payload&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;entity&lt;/code&gt; payload is configured to include the user’s unique ID, retrieved earlier, as the &lt;code&gt;author&lt;/code&gt;. The &lt;code&gt;author&lt;/code&gt; field follows the format &lt;code&gt;urn:li:person:${userId}&lt;/code&gt;. Additionally, we specify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;lifecycleState&lt;/code&gt; as &lt;code&gt;"PUBLISHED"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;visibility&lt;/code&gt; as &lt;code&gt;"PUBLIC"&lt;/code&gt; so the post is visible to the LinkedIn network.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s the updated implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sharePost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SharePostArgs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;//Get user's unique id&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getUserId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;resourcePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/ugcPosts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`urn:li:person:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;lifecycleState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PUBLISHED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      
      &lt;span class="na"&gt;visibility&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;com.linkedin.ugc.MemberNetworkVisibility&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PUBLIC&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we define the sharing content within the &lt;code&gt;specificContent&lt;/code&gt; field of the &lt;code&gt;entity&lt;/code&gt; object. For this scenario, the &lt;code&gt;specificContent&lt;/code&gt; field includes a &lt;code&gt;com.linkedin.ugc.ShareContent&lt;/code&gt; object, which has the following properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;shareCommentary&lt;/code&gt;: Accepts &lt;code&gt;content.text&lt;/code&gt; as the main text content to display.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;shareMediaCategory&lt;/code&gt;: Specifies the type of media shared in the post (set as &lt;code&gt;"ARTICLE"&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;media&lt;/code&gt;: An array of media assets for the &lt;code&gt;"ARTICLE"&lt;/code&gt; category, where each item includes: the URL to share and a &lt;code&gt;READY&lt;/code&gt; status.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Below is the updated code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;//...&lt;/span&gt;
  &lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;//...&lt;/span&gt;
    &lt;span class="nl"&gt;specificContent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;com.linkedin.ugc.ShareContent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;shareCommentary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;shareMediaCategory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ARTICLE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;media&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;READY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;originalUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Upon successfully completing the request, the response contains a &lt;code&gt;createdEntityId&lt;/code&gt;, representing the unique ID of the created entity. We can return this value to the caller for further reference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sharePost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SharePostArgs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;//...&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createdEntityId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's it! We’ve created a function that leverages the LinkedIn API to share a post URL on behalf of a user. In the next step, we’ll expose this function as a serverless endpoint using Netlify, bringing us closer to fully automating the process of sharing articles on social media.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exposing as a Netlify serverless function
&lt;/h2&gt;

&lt;p&gt;We run the CLI command &lt;code&gt;netlify functions:create&lt;/code&gt; and follow the prompts to scaffold a new Netlify serverless function named &lt;code&gt;share-on-linkedin&lt;/code&gt;. The Netlify CLI will generate the function in the functions directory with the following initial code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/* functions/share-on-linkedin.mts */&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Context&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@netlify/functions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Return an error response&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;An error occurred&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this above code, we use TypeScript and define the function as &lt;code&gt;async&lt;/code&gt; to handle the asynchronous nature of LinkedIn API calls.&lt;/p&gt;

&lt;p&gt;Next, we update the serverless function to perform the following actions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Parse the request body to extract the content to share, &lt;/li&gt;
&lt;li&gt;Retrieve the access token from environment variables (can be set in &lt;code&gt;.env&lt;/code&gt; file in the project root),&lt;/li&gt;
&lt;li&gt;Call the &lt;code&gt;sharePost&lt;/code&gt; function (defined earlier in &lt;code&gt;linkedin.ts&lt;/code&gt; ) with the extracted parameters, and&lt;/li&gt;
&lt;li&gt;Return the created entity ID as the response.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Context&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@netlify/functions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sharePost&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../utils/linkedin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;//Retrieve the access token from environment variables&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LINKEDIN_ACCESS_TOKEN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;//Parse the request body&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;    
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;

    &lt;span class="c1"&gt;//Share the post&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createdEntityId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sharePost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TOKEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;//Return the created entity ID&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;createdEntityId&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;//...&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, the serverless function is ready. We can deploy it to Netlify and test its functionality by making POST requests to the endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the Functionality
&lt;/h2&gt;

&lt;p&gt;To test the serverless function, start a local server using the CLI command &lt;code&gt;netlify dev&lt;/code&gt;. Then, use a tool like Postman or Insomnia to send a POST request to the server endpoint with the following JSON payload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://mayashavin.com/articles/share-onbehalf-linkedin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Check out this awesome article!"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, we can create a simple form UI to interact with the serverless function API, and verify that the post is successfully shared on LinkedIn.&lt;/p&gt;

&lt;p&gt;Once the function is working as expected, let's proceed to deploy it to Netlify to make it available for use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying with Netlify
&lt;/h2&gt;

&lt;p&gt;To deploy our function to Netlify, run the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;netlify deploy &lt;span class="nt"&gt;--prod&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI deploys the function to your Netlify production environment. You can then find the function endpoint in the Netlify dashboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Make sure to configure the &lt;code&gt;LINKEDIN_ACCESS_TOKEN&lt;/code&gt; environment variable in the dashboard. This step is essential for the function to authenticate and operate correctly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Flinkedin%2Fenvironment_linkedin" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Flinkedin%2Fenvironment_linkedin" alt="Environment variable" width="685" height="92"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;We have successfully built a serverless API to share an article URL on LinkedIn on behalf of a user, leveraging the LinkedIn API JavaScript Client and Netlify serverless functions. This marks a significant step toward automating the social media sharing process for blog posts.&lt;/p&gt;

&lt;p&gt;From here, we can extend the automation workflow to include other social media platforms and scheduled tasks. For example, we could integrate platforms like X (formerly Twitter), Facebook, or BlueSky, and customize the timing and content of posts to maximize audience engagement and reach.&lt;/p&gt;

&lt;p&gt;With that, stay tuned for more updates on this series!&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;Learn about Vue 3 and TypeScript with my new book &lt;a href="https://www.oreilly.com/library/view/learning-vue/9781492098812/" rel="noopener noreferrer"&gt;Learning Vue&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;Want to support me? &lt;a href="https://www.buymeacoffee.com/VTLRKH6" rel="noopener noreferrer"&gt;Buy me a coffee&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Like this post or find it helpful? Share it 😉&lt;/p&gt;

</description>
      <category>linkedin</category>
      <category>serverless</category>
      <category>javascript</category>
      <category>netlify</category>
    </item>
    <item>
      <title>Efficient Blog Cover Image Generation with CoCover for VS Code</title>
      <dc:creator>Maya Shavin 🌷☕️🏡</dc:creator>
      <pubDate>Mon, 04 Nov 2024 13:43:20 +0000</pubDate>
      <link>https://forem.com/mayashavin/efficient-blog-cover-image-generation-with-cocover-for-vs-code-2hja</link>
      <guid>https://forem.com/mayashavin/efficient-blog-cover-image-generation-with-cocover-for-vs-code-2hja</guid>
      <description>&lt;p&gt;&lt;em&gt;As a developer, I often find myself writing technical blog posts and articles in markdown format to share my knowledge with the community. Part of my content creation routine is choosing the cover image, which helps attract readers and make the content more engaging. However, this task is always time-consuming and requires additional tools and resources, leading to a significant amount of context-switching and breaking my flow.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;To address this challenge, I've recently developed and lauched &lt;a href="https://marketplace.visualstudio.com/items?itemName=mayashavinstudio.cocover" rel="noopener noreferrer"&gt;CoCover&lt;/a&gt;, a new GitHub Copilot extension for VS Code, aiming to make your content creation process experience with markdown and VS Code more efficient and seamless.&lt;/p&gt;


  


&lt;p&gt;So what does CoCover offer? Let's take a look!&lt;/p&gt;

&lt;h2&gt;
  
  
  CoCover - The AI assistant for generating cover images and more
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=mayashavinstudio.cocover" rel="noopener noreferrer"&gt;CoCover&lt;/a&gt; - &lt;strong&gt;Co&lt;/strong&gt;pilot for &lt;strong&gt;Cover&lt;/strong&gt; image - leverages the power of GitHub CoPilot and Dalle-3 for image generation based on your content, without leaving your VS Code editor.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Ff_auto%2Cq_auto%2Fv1730363067%2Fcocover%2Fcocover_marketplace_2" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Ff_auto%2Cq_auto%2Fv1730363067%2Fcocover%2Fcocover_marketplace_2" alt="CoCover in VS Code Marketplace" width="800" height="245"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This extension adds a new &lt;a href="https://code.visualstudio.com/api/extension-guides/chat" rel="noopener noreferrer"&gt;Chat Participant&lt;/a&gt; as &lt;code&gt;@CoCover&lt;/code&gt; assistant using the new VS Code's Chat extension API, and offers the following features:&lt;/p&gt;

&lt;h3&gt;
  
  
  Generate cover image based on given content
&lt;/h3&gt;

&lt;p&gt;After selecting a section of relevant information (e.g., title and description) in your markdown file and typing &lt;code&gt;@cocover /image&lt;/code&gt;, the extension will generate a cover image based on the selected content and any additional prompt you provide in your Copilot chat. &lt;a href="https://platform.openai.com/api-keys" rel="noopener noreferrer"&gt;OpenAI API key&lt;/a&gt; is required to use this feature, as CoCover uses OpenAI's Dalle-3 model to complete the task.&lt;/p&gt;


  


&lt;p&gt;You can then preview the image in the editor and decide whether to save it locally, or upload it to Cloudinary.&lt;/p&gt;

&lt;h3&gt;
  
  
  Upload generated image to Cloudinary
&lt;/h3&gt;

&lt;p&gt;Upon selecting the &lt;a href="https://cloudinary.com/" rel="noopener noreferrer"&gt;Cloudinary&lt;/a&gt; option, CoCover will prompt you for your Cloudinary credentials for uploading, including your Cloud name, API key, and API secret. And then, it will upload the generated image to your Cloudinary account and provide you with the secure image URL, ready for use.&lt;/p&gt;


  


&lt;p&gt;Once uploaded, you can copy the image URL to your clipboard, or have CoCover insert it directly into your markdown file, as a &lt;code&gt;cover_image&lt;/code&gt; frontmatter, completing the cover image creation process.&lt;/p&gt;


  


&lt;p&gt;Another option is to save the image locally, which we'll explore next.&lt;/p&gt;

&lt;h3&gt;
  
  
  Save generated image to a local destination
&lt;/h3&gt;

&lt;p&gt;CoCover will prompt you to select a destination folder to save the generated image, in &lt;code&gt;.png&lt;/code&gt; or &lt;code&gt;.jpg&lt;/code&gt; format.&lt;/p&gt;


  


&lt;p&gt;Once saved, CoCover will also give you the option to insert the image URL into your markdown file, as a &lt;code&gt;cover_image&lt;/code&gt; frontmatter, or copy the image URL to your clipboard.&lt;/p&gt;

&lt;p&gt;With these features, CoCover aims to streamline your content creation process directly from your VS Code editor and reduce the time, effort and amount of context-switching involved in getting a professional-looking cover image for your blog posts and articles. No more searching for stock images or manually creating and uploading cover images - let CoCover do the heavy lifting for you!&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next?
&lt;/h2&gt;

&lt;p&gt;I'm excited to see how CoCover can help making your content creation experience in VS Code more delightful and to add more features such as suggesting content fixes to improve your writing quality. I'm looking forward to your feedback and suggestions for future improvements. &lt;/p&gt;

&lt;p&gt;Feel free to &lt;a href="https://marketplace.visualstudio.com/items?itemName=mayashavinstudio.cocover" rel="noopener noreferrer"&gt;install CoCover&lt;/a&gt;, give it a try, and let me know what you think! Or star 🌟 it on &lt;a href="https://github.com/mayashavin/cocover" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;Learn about Vue 3 and TypeScript with my new book &lt;a href="https://www.oreilly.com/library/view/learning-vue/9781492098812/" rel="noopener noreferrer"&gt;Learning Vue&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;If you'd like to catch up with me sometimes, follow me on &lt;a href="https://x.com/MayaShavin" rel="noopener noreferrer"&gt;X&lt;/a&gt; | &lt;a href="https://www.linkedin.com/in/mayashavin" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Like this post or find it helpful? Share it 👇🏼 😉&lt;/p&gt;

</description>
      <category>vscode</category>
      <category>githubcopilot</category>
      <category>ai</category>
      <category>llm</category>
    </item>
    <item>
      <title>Effective Visual Regression Testing for Developers: Vitest vs Playwright</title>
      <dc:creator>Maya Shavin 🌷☕️🏡</dc:creator>
      <pubDate>Wed, 30 Oct 2024 06:27:15 +0000</pubDate>
      <link>https://forem.com/mayashavin/effective-visual-regression-testing-for-developers-vitest-vs-playwright-3la</link>
      <guid>https://forem.com/mayashavin/effective-visual-regression-testing-for-developers-vitest-vs-playwright-3la</guid>
      <description>&lt;p&gt;&lt;em&gt;Visual regression testing plays a crucial role in ensuring the UI and UX's consistency across different browsers, devices, and even screen sizes, especially for large applications. In this post, we will covers snapshot and pixel-to-pixel comparison with Vitest and Playwright, explore limitations, and provide insights into choosing the right visual testing approach for your project.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Table of Contents&lt;/li&gt;
&lt;li&gt;Prequisites&lt;/li&gt;
&lt;li&gt;What is visual regression testing?&lt;/li&gt;
&lt;li&gt;
Using snapshot for visual regression comparison in Vitest

&lt;ul&gt;
&lt;li&gt;Limitations of snapshot visual testing&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Pixel-to-pixel screenshot testing in Playwright&lt;/li&gt;

&lt;li&gt;Screenshot visual testing with Vitest's browser mode and Playwright&lt;/li&gt;

&lt;li&gt;Summary&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prequisites
&lt;/h2&gt;

&lt;p&gt;For the demo purpose, we will use a Vue 3 project with Vitest and Playwright installed, with a &lt;code&gt;SearchBox&lt;/code&gt; component as follows:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2FSearchbox" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2FSearchbox" alt="Search input box component" width="648" height="178"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The implementation of the &lt;code&gt;SearchBox&lt;/code&gt; component is available in &lt;a href="https://mayashavin.com/articles/component-testing-browser-vitest#the-searchbox-component" rel="noopener noreferrer"&gt;this post&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For React developers&lt;/strong&gt;, you can replace the Vue component with a React one, and Vue Test Utils with React Testing Library to render component. The rest of the steps below remains the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is visual regression testing?
&lt;/h2&gt;

&lt;p&gt;Visual regression testing (&lt;em&gt;or visual testing&lt;/em&gt;) is a technique to verify the visual appearance and usability of an application's UI between changes, helping to identify any unintended bugs that might occur to the existing functionalities during the development process. It focuses solely on validating the visual aspects of a component that a user sees or interacts with, including layout, styling, and other visual elements.&lt;/p&gt;

&lt;p&gt;There are several types of visual testing, including DOM-based comparison (snapshot testing), pixel-based comparison and manual comparison using screenshots. Depending on the requirements, we can implement visual testing at different levels with different tools, such as unit testing with Vitest or E2E testing with Playwright, or combining the two.&lt;/p&gt;

&lt;p&gt;We'll start with visual unit testing using Vitest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using snapshot for visual regression comparison in Vitest
&lt;/h2&gt;

&lt;p&gt;Vitest provides a &lt;code&gt;toMatchSnapshot()&lt;/code&gt; method to take a snapshot of the component and compare it with an existing one, as seen in the following test for &lt;code&gt;SearchBox&lt;/code&gt; Vue component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@vitest/vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;SearchBox&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./SearchBox.vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SearchBox component&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;should match snapshot&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wrapper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SearchBox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="cm"&gt;/** props and other global plugins */&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toMatchSnapshot&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Upon the first run, Vitest will take a snapshot of the &lt;code&gt;SearchBox&lt;/code&gt; component and store it in a &lt;code&gt;.snap&lt;/code&gt; file within the &lt;code&gt;__snapshots__&lt;/code&gt; folder:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2Fsnapshot_folder" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2Fsnapshot_folder" alt="Screenshot of the snapshot file's location" width="416" height="253"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Below shows an example of the snapshot file, containing the component's DOM structures and HTML layout:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2Fsnapshot_vitest.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2Fsnapshot_vitest.gif" alt="Screenshot of how a snapshot file for SearchBox looks like" width="800" height="497"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the next run, Vitest will generate a new snapshot and compare it with the stored one. If they are different, for example, when we modify the input's placeholder to &lt;code&gt;Search for a beer&lt;/code&gt;, the test will fail and Vitest will highlight the difference will in the terminal as follows:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2Ffailed_snapshot_test" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2Ffailed_snapshot_test" alt="Screenshot of how a snapshot file for SearchBox looks like" width="468" height="110"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Base on this result, we can decide whether to re-visit the changes, or update the snapshot file with the &lt;code&gt;-u&lt;/code&gt; flag in the Vitest run command to accept them.&lt;/p&gt;

&lt;p&gt;With that, we have enabled snapshot visual testing for our &lt;code&gt;SearchBox&lt;/code&gt; component. However, there is a downside of using this method, which we will discuss in the next section.&lt;/p&gt;

&lt;h3&gt;
  
  
  Limitations of snapshot visual testing
&lt;/h3&gt;

&lt;p&gt;Regular snapshot visual testing, unfortunately, does not fully provide a great developer experience, and high reliability level in visual consistency. It only compares the DOM structure and basic HTML layout of the component generated, without considering other styling aspects like colors, fonts, etc. For instance, if we change the input field &lt;code&gt;#searchbox&lt;/code&gt;'s color from black to red:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
&lt;span class="nf"&gt;#searchbox&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;red&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The snapshot test will still pass, since it is unable to validate the style changes of the component. This limitation makes it less reliable for visual testing, especially for components with dynamic content or styles.&lt;/p&gt;

&lt;p&gt;Another limitation is the readability of the snapshot files, which can lead to a maintenance nightmare, as developers may find it hard to identify the changes in these files for large components. After all, visual means what you see, and this snapshot testing does not provide a proper "see" representation.&lt;/p&gt;

&lt;p&gt;A better alternative is using screenshot-based visual testing with Playwright, which we will discuss next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pixel-to-pixel screenshot testing in Playwright
&lt;/h2&gt;

&lt;p&gt;To perform an E2E visual regression validation, Playwright provides a &lt;code&gt;toHaveScreenshot()&lt;/code&gt; method that takes and compares screenshots of a specific component (in component testing), an element, or the whole page, as seen below for our &lt;code&gt;SearchBox&lt;/code&gt; component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/** e2e/searchbox.spec.js */&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@playwright/experimental-ct-vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;SearchBox&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../src/components/SearchBox.vue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;should match screenshot&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SearchBox&lt;/span&gt; &lt;span class="nx"&gt;searchTerm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hello&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveScreenshot&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveScreenshot&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="cm"&gt;/** other assertions */&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the first run, Playwright captures and stores the screenshots as an &lt;code&gt;.png&lt;/code&gt; files to the &lt;code&gt;__snapshots__\searchbox.spec.js-snapshots&lt;/code&gt; folder, with each file named based on the test case's name (&lt;em&gt;should match screenshot&lt;/em&gt;), the testing browser (&lt;em&gt;chrome&lt;/em&gt;) and the platform (&lt;em&gt;darwin&lt;/em&gt;), as follows:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2Fscreenshot_folder" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2Fscreenshot_folder" alt="Screenshot of the screenshot files' location" width="582" height="261"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The generated screenshot file contains the view of the full page (&lt;em&gt;1&lt;/em&gt;):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1730224858%2Farticles%2Ftesting%2Fsearchbox_page" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1730224858%2Farticles%2Ftesting%2Fsearchbox_page" alt="browser's page contains an input field with hello as initial value" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And of the component (&lt;em&gt;2&lt;/em&gt;), as follows:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2Fsearchbox_full" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2Fsearchbox_full" alt="input field with hello as initial value" width="800" height="13"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The runner also throws an error. From the second run onwards, Playwright will compare the new screenshots with the current stored ones, and highlight the pixel difference (with ratio) in the terminal, such as when we change the input's font color to red:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2Fpixel_error_terminal" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2Fpixel_error_terminal" alt="failed screenshot comparison with 65 pixels difference" width="800" height="123"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An image of the expected result, the actual result, and the difference between them is also available in the test result dashboard in the browser:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2Fdifference_screenshot" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2Fdifference_screenshot" alt="actual vs expected side by side" width="800" height="610"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Additionally, we can adjust different the screenshot matcher's configurations like the maximum pixel difference &lt;code&gt;maxPixelDifference&lt;/code&gt;, and &lt;code&gt;threshold&lt;/code&gt; level by providing them as options to the &lt;code&gt;toHaveScreenshot()&lt;/code&gt; method as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveScreenshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;maxPixelDifference&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the above settings, Playwright will allow a maximum of 100 pixels difference and a threshold of 0.1 for the screenshot comparison. This way, we can fine-tune our visual testing to meet the project's requirements.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;toHaveScreenshot()&lt;/code&gt; method is a great tool, as it provides a pixel-to-pixel visual comparison for the component. However, it only works with native Playwright tests, and isn't supported in Vitest's browser mode. For that, we need to use &lt;code&gt;screenshot()&lt;/code&gt; method in the next section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Screenshot visual testing with Vitest's browser mode and Playwright
&lt;/h2&gt;

&lt;p&gt;We first need to install the relevant packages and enable the browser mode in Vitest, following &lt;a href="https://mayashavin.com/articles/component-testing-browser-vitest" rel="noopener noreferrer"&gt;this tutorial&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Once enabled, Vitest will then use Playwright to run all the tests in browser mode. Similar to the native Playwright, within a test, we can use &lt;code&gt;screenshot()&lt;/code&gt; method to take a screenshot of a specific element, such as &lt;code&gt;input&lt;/code&gt;, and save it to the &lt;code&gt;path&lt;/code&gt; provided:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**... */&lt;/span&gt;
  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;renders search input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/**... */&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByTestId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;search-input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;screenshots/searchbox_default_full.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above code results in a screenshot of the input field, as shown below:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2Fsearchbox_default_full" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2Fsearchbox_default_full" alt="input field with hello as initial value" width="228" height="34"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For the page's screenshot, we use the &lt;code&gt;page&lt;/code&gt; object, from &lt;code&gt;@vitest/browser/context&lt;/code&gt;, which is similar to the native Playwright's &lt;code&gt;page&lt;/code&gt; object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@vitest/browser/context&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="cm"&gt;/**... */&lt;/span&gt;
  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;renders search input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/**... */&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;screenshots/searchbox_page.png&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Similar to &lt;code&gt;toHaveScreenshot()&lt;/code&gt;, we can also adjust the screenshot's configuration by providing additional options to the &lt;code&gt;screenshot()&lt;/code&gt; method, such as &lt;code&gt;type&lt;/code&gt;, &lt;code&gt;clip&lt;/code&gt;, &lt;code&gt;omitBackground&lt;/code&gt;, etc., to define the file format, or capture the component's specific area.&lt;/p&gt;

&lt;p&gt;Upon running the test, Vitest will take the screenshots and store them accordingly, ready for us to manually verify the UI, or integrate with an analyzer such as &lt;a href="https://applitools.com/" rel="noopener noreferrer"&gt;Applitools&lt;/a&gt; or &lt;a href="https://www.browserstack.com/docs/percy/integrate/playwright" rel="noopener noreferrer"&gt;Percy&lt;/a&gt; for visual comparison in an automated workflow.&lt;/p&gt;

&lt;p&gt;But that's also the limitation of using Playwright's &lt;code&gt;screenshot()&lt;/code&gt;, as it requires manual verification or integration with third-party tools, which can generate additional costs. At the time of writing, Vitest browser mode does not support Playwright's &lt;code&gt;toHaveScreenshot()&lt;/code&gt; method or &lt;code&gt;toMatchImageSnapshot()&lt;/code&gt; matcher from &lt;a href="https://vitest.dev/guide/snapshot#image-snapshots" rel="noopener noreferrer"&gt;Jest-Image-Snapshot&lt;/a&gt;, which would have been a better option.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;We have explored briefly different methods of visual testing with Vitest and Playwright, from manual to automated visual comparison with snapshot and screenshot, and the pros and cons of each approach. It is essential to consider your project's requirements and budget for choosing the suitable testing approach and keep the process efficient.&lt;/p&gt;

&lt;p&gt;What's next? How about trying it out yourself and see which method works best for your project? Let me know your thoughts in the comments below!&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;Learn about Vue 3 and TypeScript with my new book &lt;a href="https://www.oreilly.com/library/view/learning-vue/9781492098812/" rel="noopener noreferrer"&gt;Learning Vue&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;Follow me on &lt;a href="https://x.com/MayaShavin" rel="noopener noreferrer"&gt;X&lt;/a&gt; | &lt;a href="https://www.linkedin.com/in/mayashavin" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Like this post or find it helpful? Share it 👇🏼 or &lt;a href="https://www.buymeacoffee.com/VTLRKH6" rel="noopener noreferrer"&gt;Buy me a coffee&lt;/a&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>vue</category>
      <category>playwrightjs</category>
      <category>vitest</category>
    </item>
    <item>
      <title>Reliable Component Testing with Vitest's Browser Mode and Playwright</title>
      <dc:creator>Maya Shavin 🌷☕️🏡</dc:creator>
      <pubDate>Tue, 08 Oct 2024 14:10:00 +0000</pubDate>
      <link>https://forem.com/mayashavin/reliable-component-testing-with-vitests-browser-mode-and-playwright-k9m</link>
      <guid>https://forem.com/mayashavin/reliable-component-testing-with-vitests-browser-mode-and-playwright-k9m</guid>
      <description>&lt;p&gt;&lt;em&gt;Vitest is great for unit testing. But for frontend components that rely on user interactions, browser events, and other visual states, unit testing alone is not enough. We also need to ensure the component looks and behaves as expected in an actual browser. And to simulate the browser environment, Vitest requires packages like JSDOM or HappyDOM, which are not always reliable as the real ones.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;An alternative is to use &lt;a href="https://mayashavin.com/articles/component-testing-router-playwright" rel="noopener noreferrer"&gt;Playwright's Component Testing&lt;/a&gt;. However, this solution requires separate setup and run, which can be cumbersome in many cases.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is where Vitest's browser mode comes in.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Table of Contents&lt;/li&gt;
&lt;li&gt;Prequisites&lt;/li&gt;
&lt;li&gt;The SearchBox component&lt;/li&gt;
&lt;li&gt;Enable Vitest's browser mode with Playwright&lt;/li&gt;
&lt;li&gt;Add the first browser test for SearchBox&lt;/li&gt;
&lt;li&gt;Using the workspace configuration file&lt;/li&gt;
&lt;li&gt;Run and view the results&lt;/li&gt;
&lt;li&gt;Summary&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prequisites
&lt;/h2&gt;

&lt;p&gt;You should have a Vue project set up with &lt;a href="https://router.vuejs.org/" rel="noopener noreferrer"&gt;Vue Router&lt;/a&gt; and &lt;a href="https://vitejs.dev/guide/features.html#testing" rel="noopener noreferrer"&gt;Vitest&lt;/a&gt;. If you haven't done so, refer to &lt;a href="https://mayashavin.com/articles/testing-components-with-vitest" rel="noopener noreferrer"&gt;this post&lt;/a&gt; to set up the essentisal Vitest testing environment for your Vue project.&lt;/p&gt;

&lt;p&gt;Once ready, let's create our testing component &lt;code&gt;SearchBox&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The SearchBox component
&lt;/h2&gt;

&lt;p&gt;Our SearchBox component accepts a search term and syncs it with the URL query params. Its template is as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;  &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"searchbox"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Search&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;v-model=&lt;/span&gt;&lt;span class="s"&gt;"search"&lt;/span&gt; 
    &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Search for a pizza"&lt;/span&gt; 
    &lt;span class="na"&gt;data-testid=&lt;/span&gt;&lt;span class="s"&gt;"search-input"&lt;/span&gt; 
    &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"searchbox"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the &lt;code&gt;script&lt;/code&gt; section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useRouter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vue-router&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useSearch&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;../composables/useSearch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;watch&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defineProps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;searchTerm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRouter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;search&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSearch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;defaultSearch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchTerm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prevValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;prevValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;search&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;immediate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in the browser, it will look like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2FSearchbox" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2FSearchbox" alt="Search input box component" width="648" height="178"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, we will set up the browser mode for Vitest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enable Vitest's browser mode with Playwright
&lt;/h2&gt;

&lt;p&gt;In &lt;code&gt;vitest.config.js&lt;/code&gt;, we will setup &lt;code&gt;browser&lt;/code&gt; mode as below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vitest.config.js&lt;/span&gt;
&lt;span class="cm"&gt;/*...*/&lt;/span&gt;
&lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/**... */&lt;/span&gt;
    &lt;span class="na"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chromium&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;playwright&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;providerOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In which, we configure the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;enabled&lt;/code&gt;: enable the browser mode&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;name&lt;/code&gt;: the browser to run the tests in (&lt;code&gt;chromium&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;provider&lt;/code&gt;: the test provider for running the browser, such as &lt;code&gt;playwright&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;providerOptions&lt;/code&gt;: additional configuration for the test provider.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We also specify which folder (&lt;code&gt;tests\browser&lt;/code&gt;) and the file convention to use, avoiding any conflicts with any existing regular unit tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vitest.config.js&lt;/span&gt;
&lt;span class="cm"&gt;/*...*/&lt;/span&gt;
&lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/**... */&lt;/span&gt;
    &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tests/browser/**/*.{spec,test}.{js,ts}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that, we are ready to write our first browser test for &lt;code&gt;SearchBox&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add the first browser test for SearchBox
&lt;/h2&gt;

&lt;p&gt;In the &lt;code&gt;tests/browser&lt;/code&gt; folder, we create a new file &lt;code&gt;SearchBox.spec.js&lt;/code&gt; with the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**SearchBox.spec.js */&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;describe&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vitest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;SearchBox&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/components/SearchBox.vue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SearchBox&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;renders search input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/** Test logic here */&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To render &lt;code&gt;SearchBox&lt;/code&gt;, we use &lt;code&gt;render()&lt;/code&gt; from &lt;code&gt;vitest-browser-vue&lt;/code&gt;, and pass the initial search term as a &lt;code&gt;prop&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**SearchBox.spec.js */&lt;/span&gt;
&lt;span class="cm"&gt;/**... */&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vitest-browser-vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SearchBox&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;renders search input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SearchBox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;searchTerm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hello&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since &lt;code&gt;SearchBox&lt;/code&gt; is using &lt;code&gt;router&lt;/code&gt; from &lt;code&gt;useRouter()&lt;/code&gt; from Vue Router, we need the following router setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a mock router using &lt;code&gt;createRouter()&lt;/code&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="cm"&gt;/** SearchBox.spec.js */&lt;/span&gt;
  &lt;span class="cm"&gt;/**... */&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;routes&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createRouter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createWebHistory&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vue-router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createRouter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;history&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;createWebHistory&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Pass it as a global plugin to &lt;code&gt;render()&lt;/code&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="cm"&gt;/** SearchBox.spec.js */&lt;/span&gt;
    &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;renders search input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SearchBox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="cm"&gt;/**... */&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="na"&gt;global&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once done, we locate the input element by its &lt;code&gt;data-testid&lt;/code&gt;, and assert its initial value using &lt;code&gt;toHaveValue()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;renders search input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/**... */&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByTestId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;search-input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;element&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toHaveValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hello&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note here &lt;code&gt;input&lt;/code&gt; received is just a &lt;code&gt;Locator&lt;/code&gt; and not a valid HTML element. We need &lt;code&gt;input.element()&lt;/code&gt; to get the HTML instance. Otherwise, Vitest will throw the below error:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2Ferror_element" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2Ferror_element" alt="Error of value needed to be HTML or SVG element" width="800" height="93"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To change the input's value, we use &lt;code&gt;input.fill()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;renders search input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/**... */&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, we can use &lt;code&gt;userEvent()&lt;/code&gt; from &lt;code&gt;@vitest/browser/context&lt;/code&gt; as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;userEvent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@vitest/browser/context&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="cm"&gt;/**... */&lt;/span&gt;
  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;renders search input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/**... */&lt;/span&gt;    
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;userEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both approaches perform the same. We can then assert the new value as usual:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;element&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toHaveValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it! We have successfully written our first browser test.&lt;/p&gt;

&lt;p&gt;At this point, we have one test configuration set for our Vitest runner. This setup will be problematic when Vitest need to run both unit and browser tests together in an automation workflow. For such cases, we use workspace and separate the settings per test type, which we explore next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using the workspace configuration file
&lt;/h2&gt;

&lt;p&gt;We create a new file &lt;code&gt;vitest.workspace.js&lt;/code&gt; to store the workspace configurations as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineWorkspace&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vitest/config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineWorkspace&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;extends&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vitest.config.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jsdom&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;**/*/unit/*.{spec,test}.{js,ts}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In which, we define the first configuration for &lt;code&gt;unit&lt;/code&gt; tests using &lt;code&gt;jsdom&lt;/code&gt;, based on the existing &lt;code&gt;vitest.config.js&lt;/code&gt; settings. We also specify the folder and file convention for the unit tests.&lt;/p&gt;

&lt;p&gt;Similarly, we define the second configuration for &lt;code&gt;browser&lt;/code&gt; tests using &lt;code&gt;playwright&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineWorkspace&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="cm"&gt;/** ... */&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;extends&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vitest.config.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;**/*/browser/*.{spec,test}.{js,ts}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chromium&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;playwright&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;browser&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And with that, we can run all our tests in a single command, which we will see next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run and view the results
&lt;/h2&gt;

&lt;p&gt;We add the following command to our &lt;code&gt;package.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vitest --workspace=vitest.workspace.js"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Upon executing &lt;code&gt;yarn test&lt;/code&gt;, Vitest runs the tests based on &lt;code&gt;vitest.workspace.js&lt;/code&gt; and displays the results in a GUI dashboard as follows:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2FVitest_UI" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Ff_auto%2Fv1727777568%2Farticles%2Ftesting%2FVitest_UI" alt="Dashboard of Vitest result with each test labeled by type" width="800" height="386"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Vitest labels each test by &lt;code&gt;unit&lt;/code&gt; or &lt;code&gt;browser&lt;/code&gt; status. We can then filter the tests by their statuses, or perform further debugging with the given browser UI per test suite.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;We have learned how to set up browser mode for Vitest using Playwright, and write the first browser test. We have also explored how to take screenshots for visual testing, and use the workspace configuration to separate the settings per testing mode. One big limitation of Vitest's browser mode in comparison to Playwright's Component Testing is the lack of browser's address bar, limiting us from testing the component's state synchronization with URL query params in the browser. But it's a good start to build a scalable testing strategy for our Vue projects.&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;Learn about Vue 3 and TypeScript with my new book &lt;a href="https://www.oreilly.com/library/view/learning-vue/9781492098812/" rel="noopener noreferrer"&gt;Learning Vue&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;Follow me on &lt;a href="https://x.com/MayaShavin" rel="noopener noreferrer"&gt;X&lt;/a&gt; | &lt;a href="https://www.linkedin.com/in/mayashavin" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Like this post or find it helpful? Share it 👇🏼 or &lt;a href="https://www.buymeacoffee.com/VTLRKH6" rel="noopener noreferrer"&gt;Buy me a coffee&lt;/a&gt;&lt;/p&gt;

</description>
      <category>testing</category>
      <category>vue</category>
      <category>playwrightjs</category>
      <category>vitest</category>
    </item>
    <item>
      <title>Seamless Contact Form experience with Netlify Form in Nuxt 3</title>
      <dc:creator>Maya Shavin 🌷☕️🏡</dc:creator>
      <pubDate>Wed, 07 Aug 2024 09:04:08 +0000</pubDate>
      <link>https://forem.com/mayashavin/seamless-contact-form-experience-with-netlify-form-in-nuxt-3-3amn</link>
      <guid>https://forem.com/mayashavin/seamless-contact-form-experience-with-netlify-form-in-nuxt-3-3amn</guid>
      <description>&lt;p&gt;&lt;em&gt;Contact form is an essential part of any portfolio site, where people can reach you for further queries and questions. In this article, we will explore how to use Netlify Form service to create a contact form and handle its submission from end to end in a Nuxt static site.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You need to have &lt;a href="https://app.netlify.com/signup" rel="noopener noreferrer"&gt;an active Netlify account&lt;/a&gt;, and create a Nuxt 3 application using the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx nuxi@latest init your-nuxt-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once done, we are ready to build our contact form, starting with enabling the Netlify Form service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enabling Netlify Form service
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.netlify.com/forms/setup/" rel="noopener noreferrer"&gt;Netlify Serverless Form&lt;/a&gt; is an out-of-the-box service from Netlify that allows us to manage forms in our static applications at ease, without extra API calls or server setup. &lt;/p&gt;

&lt;p&gt;In the Netlify dashboard, we navigate to our target site project, select the &lt;strong&gt;Form&lt;/strong&gt; section on the left sidebar, and scroll down to the &lt;strong&gt;Form Detection&lt;/strong&gt; section. Here, we can enable the form detection by clicking the &lt;strong&gt;Enable Form detection&lt;/strong&gt; button.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Cf_auto%2Fv1722972945%2Farticles%2FNuxt%2Fnetlify_form_not_enabled" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Cf_auto%2Fv1722972945%2Farticles%2FNuxt%2Fnetlify_form_not_enabled" alt="Netlify Form Detection in disabled mode"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once enabled, Netlify is ready to detect and manage our target HTML form during the deployment process. But to fullly use this feature, we need to create the form with the necessary Netlify attributes, which we will do next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a contact form
&lt;/h2&gt;

&lt;p&gt;In our Nuxt application, let's create a &lt;code&gt;contact.vue&lt;/code&gt; page, located in &lt;code&gt;pages/&lt;/code&gt; directory. This page contains an HTML &lt;code&gt;form&lt;/code&gt; with the following attributes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;netlify&lt;/code&gt; or &lt;code&gt;data-netlify="true"&lt;/code&gt; - Netlify detects the target form using this attribute.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;data-netlify-honeypot&lt;/code&gt; - Netlify uses this attribute to locate a hidden "honeypot" field for spam protection.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;name&lt;/code&gt; - unique name of the form to appear in the Netlify Form panel.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;method&lt;/code&gt; - HTTP method to use when submitting the form.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;id&lt;/code&gt; - The unique identifier for the form.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; 
  &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"contact-form"&lt;/span&gt; 
  &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt;
  &lt;span class="na"&gt;netlify&lt;/span&gt;
  &lt;span class="na"&gt;data-netlify=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt; 
  &lt;span class="na"&gt;data-netlify-honeypot=&lt;/span&gt;&lt;span class="s"&gt;"bot-field"&lt;/span&gt; 
  &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"contact-form"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- form fields --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the spam protection, we add a hidden &lt;code&gt;input&lt;/code&gt; as the "honeybot" field, which is visible to bots but not for real users. If the field is filled, Netlify will reject the submission. Its &lt;code&gt;name&lt;/code&gt; should be identical with the value of &lt;code&gt;data-netlify-honeypot&lt;/code&gt;, as shown below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"hidden"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&amp;gt;&lt;/span&gt;
    Don’t fill this out if you’re human: &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"bot-field"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And since we are working with static Nuxt site (SSR pre-rendering mode), we need to add an additional hidden input field with the name &lt;code&gt;form-name&lt;/code&gt; and the &lt;code&gt;value&lt;/code&gt; of the form's name, as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"hidden"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"form-name"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"contact-form"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we add the actual form fields for the user to fill in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"label"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    Your Name
  &lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt; 
    &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"What is your name?"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"field"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"label"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    Your Email
  &lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt; 
    &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"What is your email?"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"field"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"label"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    Your Message
  &lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;textarea&lt;/span&gt; &lt;span class="na"&gt;rows=&lt;/span&gt;&lt;span class="s"&gt;"4"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt; 
    &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"What do you want to talk about?"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"field"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And some &lt;code&gt;button&lt;/code&gt; for submitting and reseting the form when needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Send message&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"reset"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Clear&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, we have created a simple contact form with the necessary attributes for Netlify Form to detect. Upon a successful form submission, Netlify will redirect the user to a default form's success page. We can override this behavior by giving a custom success page URL with the &lt;code&gt;action&lt;/code&gt; attribute in the &lt;code&gt;form&lt;/code&gt; element.&lt;/p&gt;

&lt;p&gt;Or, we can stop the redirecting mechanism and handle the form submission programatically, which we will do next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling form submission with JavaScript
&lt;/h2&gt;

&lt;p&gt;In our page's &lt;code&gt;script&lt;/code&gt; section, we define a &lt;code&gt;handleSubmit&lt;/code&gt; method to handle the form submission. We use the &lt;code&gt;fetch&lt;/code&gt; API to send the form data as a URL-encoded string using the &lt;code&gt;FormData&lt;/code&gt; API, with the content type of &lt;code&gt;"application/x-www-form-urlencoded"&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleSubmit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/x-www-form-urlencoded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To manage the form submission's status, we define the &lt;code&gt;FormState&lt;/code&gt; enum and &lt;code&gt;formState&lt;/code&gt; variable as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FormState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;IDLE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;IDLE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;PENDING&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PENDING&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;SUCCESS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SUCCESS&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ERROR&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;contactFormState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;FormState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;FormState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;IDLE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in the &lt;code&gt;handleSubmit&lt;/code&gt; method, we update the state accordingly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleSubmit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;contactFormState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;FormState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PENDING&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="cm"&gt;/**... */&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/**... */&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;contactFormState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;FormState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SUCCESS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;contactFormState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;FormState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/**... */&lt;/span&gt;
    &lt;span class="nx"&gt;contactFormState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;FormState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can also reset the form's data once the form is submitted successfully, as shown below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="cm"&gt;/** ... */&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;contactFormState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;FormState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SUCCESS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And we reset the form state back to &lt;code&gt;IDLE&lt;/code&gt; after a few seconds for re-submission:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="cm"&gt;/**... */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="cm"&gt;/**... */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;contactFormState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;FormState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;IDLE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We then bind the form submission event &lt;code&gt;submit&lt;/code&gt; to &lt;code&gt;handleSubmit&lt;/code&gt;, with the modifier &lt;code&gt;.prevent&lt;/code&gt; for &lt;code&gt;preventDefault()&lt;/code&gt;, as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt;
  &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"contact-form"&lt;/span&gt;
  &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;submit.prevent=&lt;/span&gt;&lt;span class="s"&gt;"handleSubmit"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, we display a custom message to the user based on the form's state as below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!--contact.vue--&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"formState === FormState.SUCCESS"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-green-400 text-center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Form has been submitted successfully!
&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;v-else-if=&lt;/span&gt;&lt;span class="s"&gt;"formState === FormState.ERROR"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-red-500 text-center"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Form submission failed. Please try again.
&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, our contact page will look similar to the following upon a successful submission:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Cf_auto%2Fv1722972945%2Farticles%2FNuxt%2Fsuccessful_submission_1" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Cf_auto%2Fv1722972945%2Farticles%2FNuxt%2Fsuccessful_submission_1" alt="Contact form after submission"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With that, our app is ready for deployment. However, when we use Nuxt 3 in SSR pre-rendering mode (or static site generating mode), Netlify is sometimes unable to detect and parse the form properly during the build process. We need an additional workaround to make sure that Netlify detects the form.&lt;/p&gt;

&lt;p&gt;Let's do it in the next section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making sure Netlify detects the form on deployment
&lt;/h2&gt;

&lt;p&gt;To do so, in the &lt;code&gt;public&lt;/code&gt; directory, we create an HTML file - &lt;code&gt;contact-duplicate.html&lt;/code&gt; - with the form's main HTML code, including all the form fields, and the required attributes for Netlify to use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"UTF-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width, initial-scale=1.0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Contact Form&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; 
    &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"contact-form"&lt;/span&gt; 
    &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt;
    &lt;span class="na"&gt;netlify&lt;/span&gt;
    &lt;span class="na"&gt;data-netlify=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt; 
    &lt;span class="na"&gt;data-netlify-honeypot=&lt;/span&gt;&lt;span class="s"&gt;"bot-field"&lt;/span&gt; 
    &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"contact-form"&lt;/span&gt;
    &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"hidden"&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!--form fields &amp;amp; buttons--&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it! Nuxt will automatically include this file in the build process, allowing Netlify to detect the form and perform the necessary actions during deployment. Now, when we deploy our site to Netlify for the first time, it will detect the form and add it to the system automatically.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Cf_auto%2Fv1722972945%2Farticles%2FNuxt%2Factiveforms_netlify" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Fq_auto%2Cf_auto%2Fv1722972945%2Farticles%2FNuxt%2Factiveforms_netlify" alt="Netlify Active forms in the dashboard"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;In this post, we learned how to create and manage a contact form submission using Netlify Form in Nuxt SSR pre-rendering mode. We also learned how to add basic spam protection to the form, handle the form submission programmatically using JavaScript, and display a custom message to the user based on the form's state without redirecting the user.&lt;/p&gt;

&lt;p&gt;What's next? How about adding more validations or integrate a third-party service like &lt;a href="https://vee-validate.logaretm.com/v4/" rel="noopener noreferrer"&gt;VeeValidate&lt;/a&gt; for better user experience?&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;Learn about Vue 3 and TypeScript with my new book &lt;a href="https://www.oreilly.com/library/view/learning-vue/9781492098812/" rel="noopener noreferrer"&gt;Learning Vue&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;If you'd like to catch up with me sometimes, follow me on &lt;a href="https://x.com/MayaShavin" rel="noopener noreferrer"&gt;X&lt;/a&gt; | &lt;a href="https://www.linkedin.com/in/mayashavin" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Like this post or find it helpful? Share it 👇🏼 😉&lt;/p&gt;

</description>
      <category>nuxt</category>
      <category>netlify</category>
      <category>vue</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Build a smart product data generator from image with GPT-4o and Langchain</title>
      <dc:creator>Maya Shavin 🌷☕️🏡</dc:creator>
      <pubDate>Tue, 25 Jun 2024 05:34:10 +0000</pubDate>
      <link>https://forem.com/mayashavin/build-a-smart-product-data-generator-from-image-with-gpt-4o-and-langchain-5gof</link>
      <guid>https://forem.com/mayashavin/build-a-smart-product-data-generator-from-image-with-gpt-4o-and-langchain-5gof</guid>
      <description>&lt;p&gt;&lt;em&gt;When listing new products to an online store, owners or marketers often find it too time-consuming to fill in the essential information such as title, description, and tags for each product from scratch. Most of the information can be retrieved from the product image itself. With the right combination of LLM and AI tools, such as Langchain and OpenAI, we can automate the process of writing product's information using an input of image, which is our focus in today's post.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Table of contents&lt;/li&gt;
&lt;li&gt;
Brief introduction about Langchain and OpenAI

&lt;ul&gt;
&lt;li&gt;Setting up Langchain and OpenAI&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;The flow of generating product data&lt;/li&gt;

&lt;li&gt;Step 1: Load an product image into base64 format&lt;/li&gt;

&lt;li&gt;

Step 2: Ask GPT to generate a product's metadata

&lt;ul&gt;
&lt;li&gt;Setting up OpenAI API key&lt;/li&gt;
&lt;li&gt;Creating a model to process the image and prompt&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

Step 3: Extract the result from GPT in a structured Product format

&lt;ul&gt;
&lt;li&gt;Define the Product structure&lt;/li&gt;
&lt;li&gt;Create a function to extract the product information&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Chaining all the steps together using Langchain&lt;/li&gt;

&lt;li&gt;Resources&lt;/li&gt;

&lt;li&gt;Summary&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Brief introduction about Langchain and OpenAI
&lt;/h2&gt;

&lt;p&gt;Langchain is a powerful tool that allows you to architect and run AI-powered functions with ease. It provides a simple interface to integrate with different LLMs (Large-Language-Models) APIs and services such as OpenAI, Hugging Face, etc. It also offers an extensible architecture that allows you to create and manage custom chains (pipelines), agents, and workflows tailored to your specific needs.&lt;/p&gt;

&lt;p&gt;OpenAI is a leading AI research lab that has developed several powerful LLMs, including GPT-3, GPT-4 and Dall-E. These models can generate human-like text and media based on the input prompt, making them ideal for a wide range of applications, from chatbots to content/image generation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting up Langchain and OpenAI
&lt;/h3&gt;

&lt;p&gt;In this post, we will use GPT-4o model from OpenAI for better image anayzing and text completion, along with the following Langchain Python packages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;langchain-openai&lt;/code&gt; - A package that provides a simple interface to interact with OpenAI API.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;langchain_core&lt;/code&gt; - The core package of Langchain that provides the necessary tools to build your AI functions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To install these packages, you use the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install &lt;/span&gt;langchain-openai langchain-core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, let's define the flow of how we generate product information based on a given image.&lt;/p&gt;

&lt;h2&gt;
  
  
  The flow of generating product data
&lt;/h2&gt;

&lt;p&gt;Our tool will perform the following steps upon receiving an image URL from the user:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Load the given product image into base64 data URI text format.&lt;/li&gt;
&lt;li&gt;Ask GPT to analyze and generate the required product's metadata based on such data.&lt;/li&gt;
&lt;li&gt;Extract the result from GPT in a structured Product format.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The below diagram demonstrates how the our work flow looks like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Ff_auto%2Cq_auto%2Fv1719259328%2Farticles%2Fllm%2Fflow_generator" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Ff_auto%2Cq_auto%2Fv1719259328%2Farticles%2Fllm%2Fflow_generator" alt="Diagram flow of generating product data"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With this flow in mind, let's walk through each step's implementation in detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Load an product image into base64 format
&lt;/h2&gt;

&lt;p&gt;Before we can ask GPT to generate a product's metadata from a given image URL, we need to convert it into a format that GPT can understand, which is &lt;code&gt;base64&lt;/code&gt; data URI. To do so, we will create an &lt;code&gt;image.py&lt;/code&gt; with the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;encode_image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
  &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;image_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above &lt;code&gt;encode_function&lt;/code&gt; function takes an &lt;code&gt;image_path&lt;/code&gt;, opens and reads the image into bytes format, and then returns the encoded &lt;code&gt;based64&lt;/code&gt; text version.&lt;/p&gt;

&lt;p&gt;We then write a &lt;code&gt;load_image&lt;/code&gt; function, which performs the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Receives &lt;code&gt;inputs&lt;/code&gt; as a dictionary, which contains an &lt;code&gt;image_path&lt;/code&gt; key with the path to the image file, &lt;/li&gt;
&lt;li&gt;Reads &lt;code&gt;inputs[image_path]&lt;/code&gt; into base64 format using &lt;code&gt;base64.b64encode()&lt;/code&gt; method.&lt;/li&gt;
&lt;li&gt;Assigns the result to &lt;code&gt;image&lt;/code&gt; property of the returned object for the function.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The code is as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Load image from file and encode it as base64.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;image_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image_path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;image_base64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;encode_image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image_file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;image_base64&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we have the image processing step implemented. Next, we will create a function to communicate with GPT for the information desired based on this image data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Ask GPT to generate a product's metadata
&lt;/h2&gt;

&lt;p&gt;In this step, since we are going to send request to GPT API, we need to set up its API's key for related Langchain OpenAI package to pick up and initialize the service.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting up OpenAI API key
&lt;/h3&gt;

&lt;p&gt;The most straighforward way is to create an &lt;code&gt;.env&lt;/code&gt; file with an &lt;code&gt;OPENAI_API_KEY&lt;/code&gt; variable, whose value can be found under Settings panel, as shown below:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Ff_auto%2Cq_auto%2Fv1719259328%2Farticles%2Fllm%2Fapi" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Ff_auto%2Cq_auto%2Fv1719259328%2Farticles%2Fllm%2Fapi" alt="Screenshot of how to retrieve API key in OpenAI Panel"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-open-ai-api-key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, we install &lt;code&gt;python-dotenv&lt;/code&gt; package using the below command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install &lt;/span&gt;python-dotenv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in our &lt;code&gt;generate.py&lt;/code&gt; file, we add the following code to load the key from the &lt;code&gt;.env&lt;/code&gt; file into our project for usage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_dotenv&lt;/span&gt;

&lt;span class="nf"&gt;load_dotenv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And with that, we can implement the function that will invoke the GPT model for answers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating a model to process the image and prompt
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;generate.py&lt;/code&gt;, we create a function &lt;code&gt;image_model&lt;/code&gt; that takes &lt;code&gt;inputs&lt;/code&gt; as a dictionary containing the fields: &lt;code&gt;image&lt;/code&gt; and &lt;code&gt;prompt&lt;/code&gt;, where &lt;code&gt;image&lt;/code&gt; is the base64 data URI from step 1.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;image_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
 &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Invoke model with image and prompt.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="n"&gt;image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From the given inputs, we compute a user's message to pass to the model. To do so, we use &lt;code&gt;HumanMessage&lt;/code&gt; class from &lt;code&gt;langchain_core.messages&lt;/code&gt; package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HumanMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data:image/jpeg;base64,&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt; &lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the above code, we pass to &lt;code&gt;HumanMessage&lt;/code&gt; an array of &lt;code&gt;content&lt;/code&gt; containing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;text&lt;/code&gt; object with the &lt;code&gt;prompt&lt;/code&gt; text&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;image_url&lt;/code&gt; object with the base64-encoded &lt;code&gt;image&lt;/code&gt; data as the URL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once we have the &lt;code&gt;message&lt;/code&gt; ready, we then initialize a model instance of &lt;code&gt;ChatOpenAI&lt;/code&gt; using &lt;code&gt;gpt-4o&lt;/code&gt;, an &lt;code&gt;0.5&lt;/code&gt; temperature and a maximum number of &lt;code&gt;1024&lt;/code&gt; tokens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ChatOpenAI&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;image_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
 &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Invoke model with image and prompt.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="c1"&gt;#... previous code
&lt;/span&gt;    &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChatOpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4o&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And invoke the model with the &lt;code&gt;message&lt;/code&gt; and return the &lt;code&gt;content&lt;/code&gt; of the response, as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;image_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
 &lt;span class="c1"&gt;#... previous code
&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this stage, we have the content of the response from GPT. In the next step, we will extract that content in a structured Product format.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Extract the result from GPT in a structured Product format
&lt;/h2&gt;

&lt;p&gt;The response from GPT is always in a text format, which requires us to parse and extract the relevant information in a structured Product format. This is not a straightforward step. Fortunately, Langchain provides us several tools to help us with this task, starting with defining the output structure format.&lt;/p&gt;

&lt;h3&gt;
  
  
  Define the Product structure
&lt;/h3&gt;

&lt;p&gt;We will define a &lt;code&gt;Product&lt;/code&gt; class as a Pydantic model using &lt;code&gt;BaseModel&lt;/code&gt; and &lt;code&gt;Field&lt;/code&gt; from the &lt;code&gt;langchain.pydantic_v1&lt;/code&gt; package, as shown below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Product.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_core.pydantic_v1&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;'''&lt;/span&gt;&lt;span class="s"&gt;Product description&lt;/span&gt;&lt;span class="sh"&gt;'''&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(...,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Product Title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Title of the product&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(...,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Product Description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Description of the product&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;([],&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Product Tags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tags for SEO&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above class defines a &lt;code&gt;Product&lt;/code&gt; model with the following fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;title&lt;/code&gt; - The title of the product&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;description&lt;/code&gt; - The description of the product&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tags&lt;/code&gt; - The tags for SEO&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next, we declare a parser function that will extract the GPT response into the &lt;code&gt;Product&lt;/code&gt; structure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a function to extract the product information
&lt;/h3&gt;

&lt;p&gt;We can use &lt;code&gt;JsonOutputParser&lt;/code&gt; class to create a custom parser by passing our &lt;code&gt;Product&lt;/code&gt; structure as its &lt;code&gt;pydantic_object&lt;/code&gt;, as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_core.output_parsers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;JsonOutputParser&lt;/span&gt;

&lt;span class="c1"&gt;#... previous code
&lt;/span&gt;&lt;span class="n"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;JsonOutputParser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pydantic_object&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Great. All left is to modify our &lt;code&gt;content&lt;/code&gt; array in Step 2 to include the parser's format instructions, by adding the following element to the array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="c1"&gt;#... previous code
&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_format_instructions&lt;/span&gt;&lt;span class="p"&gt;()},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
       &lt;span class="c1"&gt;# ... code
&lt;/span&gt;    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And with that, all the components for the flow is ready. It's time to chain them together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chaining all the steps together using Langchain
&lt;/h2&gt;

&lt;p&gt;Chaining is similar to a train of action carriage, where each carriage can be a step of LLM call, data transformation, or any tool connected together, supporting streaming, async and batch processing out of the box. In our case, we will use &lt;code&gt;TransformChain&lt;/code&gt; for transforming our &lt;code&gt;image_path&lt;/code&gt; input into a proper base64 data input as a pre-processing step of the main flow.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain.chains&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TransformChain&lt;/span&gt;

&lt;span class="n"&gt;load_image_chain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TransformChain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;input_variables&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;image_path&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;output_variables&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;load_image&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From there, we create another &lt;code&gt;generate_product_chain&lt;/code&gt; that chains all the flow components together using &lt;code&gt;|&lt;/code&gt; operator, starting with loading and transforming the image path into a base64 data URI text, then passing its output as the input to our image model for generating the desired data, and finally parsing the result into our target Product format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;generate_product_chain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;load_image_chain&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;image_model&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, we define &lt;code&gt;get_product_info&lt;/code&gt; function to invoke the chain with the initial input &lt;code&gt;image_path&lt;/code&gt; and &lt;code&gt;prompt&lt;/code&gt; as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_product_info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="n"&gt;generate_product_chain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;load_image_chain&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;image_model&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;

&lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
   Given the image of a product, provide the following information:
   - Product Title
   - Product Description
   - At least 13 Product Tags for SEO purposes
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;generate_product_chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;image_path&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;image_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;prompt&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's it! We have successfully built a smart product information generator. You can now use the &lt;code&gt;get_product_info&lt;/code&gt; function to generate product information by giving it a valid image path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;product_info&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_product_info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path/to/image.jpg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_info&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Ff_auto%2Cq_auto%2Fv1719259328%2Farticles%2Fllm%2Fexample_flow_generate" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fmayashavin%2Fimage%2Fupload%2Ff_auto%2Cq_auto%2Fv1719259328%2Farticles%2Fllm%2Fexample_flow_generate" alt="Diagram flow of generating product data"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://python.langchain.com/v0.2/docs/introduction/" rel="noopener noreferrer"&gt;Langchain documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://platform.openai.com/docs/overview" rel="noopener noreferrer"&gt;OpenAI API documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/mayashavin/product-info-ai-generator" rel="noopener noreferrer"&gt;Product Information Generator Repo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;In this post, we have explored how to generate essential product data such as title, description and tags based on a given image using Langchain, Open AI GPT-4o. We have walked through the flow, including loading an image into base64 text format, asking GPT to generate a product's metadata, and extracting the result from GPT in a structured Product format. We have also seen how to chain all the steps together using Langchain to create a working product information generator.&lt;/p&gt;

&lt;p&gt;In the next post, we will explore how to deploy this tool as a web service API using Flask. Until then, happy coding!&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;Learn about Vue 3 and TypeScript with my new book &lt;a href="https://www.oreilly.com/library/view/learning-vue/9781492098812/" rel="noopener noreferrer"&gt;Learning Vue&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;👉 &lt;em&gt;If you'd like to catch up with me sometimes, follow me on &lt;a href="https://x.com/MayaShavin" rel="noopener noreferrer"&gt;X&lt;/a&gt; | &lt;a href="https://www.linkedin.com/in/mayashavin" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Like this post or find it helpful? Share it 👇🏼 😉&lt;/p&gt;

</description>
      <category>openai</category>
      <category>langchain</category>
      <category>python</category>
      <category>tutorials</category>
    </item>
  </channel>
</rss>
